Bug 967742 - Consolidate API to edit HomeConfig state (r=margaret)
authorLucas Rocha <lucasr@mozilla.com>
Fri, 07 Mar 2014 17:15:31 +0000
changeset 172494 d184b2d30734084cce49c65a0e3693b49b12edc2
parent 172493 cc50c0d103cbc1be642f438ddbfea1793e2eb623
child 172495 51f11b099596c5ade911975fe9135e52ad9e355e
push id26363
push userryanvm@gmail.com
push dateFri, 07 Mar 2014 20:25:45 +0000
treeherdermozilla-central@0d70e6efa22c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs967742
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 967742 - Consolidate API to edit HomeConfig state (r=margaret)
mobile/android/base/home/HomeConfig.java
mobile/android/base/home/HomeConfigInvalidator.java
mobile/android/base/home/HomeConfigLoader.java
mobile/android/base/home/HomePager.java
mobile/android/base/home/HomePanelPicker.java
mobile/android/base/preferences/PanelsPreferenceCategory.java
--- a/mobile/android/base/home/HomeConfig.java
+++ b/mobile/android/base/home/HomeConfig.java
@@ -1,28 +1,33 @@
 /* -*- 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 org.mozilla.gecko.R;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 
 public final class HomeConfig {
     /**
      * Used to determine what type of HomeFragment subclass to use when creating
      * a given panel. With the exception of DYNAMIC, all of these types correspond
      * to a default set of built-in panels. The DYNAMIC panel type is used by
      * third-party services to create panels with varying types of content.
@@ -260,29 +265,29 @@ public final class HomeConfig {
         public boolean isDynamic() {
             return (mType == PanelType.DYNAMIC);
         }
 
         public boolean isDefault() {
             return mFlags.contains(Flags.DEFAULT_PANEL);
         }
 
-        public void setIsDefault(boolean isDefault) {
+        private void setIsDefault(boolean isDefault) {
             if (isDefault) {
                 mFlags.add(Flags.DEFAULT_PANEL);
             } else {
                 mFlags.remove(Flags.DEFAULT_PANEL);
             }
         }
 
         public boolean isDisabled() {
             return mFlags.contains(Flags.DISABLED_PANEL);
         }
 
-        public void setIsDisabled(boolean isDisabled) {
+        private void setIsDisabled(boolean isDisabled) {
             if (isDisabled) {
                 mFlags.add(Flags.DISABLED_PANEL);
             } else {
                 mFlags.remove(Flags.DISABLED_PANEL);
             }
         }
 
         public JSONObject toJSON() throws JSONException {
@@ -687,16 +692,384 @@ public final class HomeConfig {
 
             @Override
             public ViewConfig[] newArray(final int size) {
                 return new ViewConfig[size];
             }
         };
     }
 
+    /**
+     * Immutable representation of the current state of {@code HomeConfig}.
+     * This is what HomeConfig returns from a load() call and takes as
+     * input to save a new state.
+     *
+     * Users of {@code State} should use an {@code Iterator} to iterate
+     * through the contained {@code PanelConfig} instances.
+     *
+     * {@code State} is immutable i.e. you can't add, remove, or update
+     * contained elements directly. You have to use an {@code Editor} to
+     * change the state, which can be created through the {@code edit()}
+     * method.
+     */
+    public static class State implements Iterable<PanelConfig> {
+        private final HomeConfig mHomeConfig;
+        private final List<PanelConfig> mPanelConfigs;
+
+        private State(HomeConfig homeConfig, List<PanelConfig> panelConfigs) {
+            mHomeConfig = homeConfig;
+            mPanelConfigs = Collections.unmodifiableList(panelConfigs);
+        }
+
+        @Override
+        public Iterator<PanelConfig> iterator() {
+            return mPanelConfigs.iterator();
+        }
+
+        /**
+         * Creates an {@code Editor} for this state.
+         */
+        public Editor edit() {
+            return new Editor(mHomeConfig, this);
+        }
+    }
+
+    /**
+     * {@code Editor} allows you to make changes to a {@code State}. You
+     * can create {@code Editor} by calling {@code edit()} on the target
+     * {@code State} instance.
+     *
+     * {@code Editor} works on a copy of the {@code State} that originated
+     * it. This means that adding, removing, or updating panels in an
+     * {@code Editor} will never change the {@code State} which you
+     * created the {@code Editor} from. Calling {@code commit()} or
+     * {@code apply()} will cause the new {@code State} instance to be
+     * created and saved using the {@code HomeConfig} instance that
+     * created the source {@code State}.
+     *
+     * {@code Editor} is *not* thread-safe. You can only make calls on it
+     * from the thread where it was originally created. It will throw an
+     * exception if you don't follow this invariant.
+     */
+    public static class Editor implements Iterable<PanelConfig> {
+        private final HomeConfig mHomeConfig;
+        private final HashMap<String, PanelConfig> mConfigMap;
+        private final Thread mOriginalThread;
+
+        private PanelConfig mDefaultPanel;
+        private int mEnabledCount;
+
+        private Editor(HomeConfig homeConfig, State configState) {
+            mHomeConfig = homeConfig;
+            mOriginalThread = Thread.currentThread();
+            mConfigMap = new LinkedHashMap<String, PanelConfig>();
+            mEnabledCount = 0;
+
+            initFromState(configState);
+        }
+
+        /**
+         * Initialize the initial state of the editor from the given
+         * {@sode State}. A LinkedHashMap is used to represent the list of
+         * panels as it provides fast access to specific panels from IDs
+         * while also being order-aware. We keep a reference to the
+         * default panel and the number of enabled panels to avoid iterating
+         * through the map every time we need those.
+         *
+         * @param configState The source State to load the editor from.
+         */
+        private void initFromState(State configState) {
+            for (PanelConfig panelConfig : configState) {
+                final PanelConfig panelCopy = new PanelConfig(panelConfig);
+
+                if (!panelCopy.isDisabled()) {
+                    mEnabledCount++;
+                }
+
+                if (panelCopy.isDefault()) {
+                    if (mDefaultPanel == null) {
+                        mDefaultPanel = panelCopy;
+                    } else {
+                        throw new IllegalStateException("Multiple default panels in HomeConfig state");
+                    }
+                }
+
+                mConfigMap.put(panelConfig.getId(), panelCopy);
+            }
+
+            // We should always have a defined default panel if there's
+            // at least one enabled panel around.
+            if (mEnabledCount > 0 && mDefaultPanel == null) {
+                throw new IllegalStateException("Default panel in HomeConfig state is undefined");
+            }
+        }
+
+        private PanelConfig getPanelOrThrow(String panelId) {
+            final PanelConfig panelConfig = mConfigMap.get(panelId);
+            if (panelConfig == null) {
+                throw new IllegalStateException("Tried to access non-existing panel: " + panelId);
+            }
+
+            return panelConfig;
+        }
+
+        private boolean isCurrentDefaultPanel(PanelConfig panelConfig) {
+            if (mDefaultPanel == null) {
+                return false;
+            }
+
+            return mDefaultPanel.equals(panelConfig);
+        }
+
+        private void findNewDefault() {
+            // Pick the first panel that is neither disabled nor currently
+            // set as default.
+            for (PanelConfig panelConfig : mConfigMap.values()) {
+                if (!panelConfig.isDefault() && !panelConfig.isDisabled()) {
+                    setDefault(panelConfig.getId());
+                    return;
+                }
+            }
+
+            mDefaultPanel = null;
+        }
+
+        private List<PanelConfig> makeDeepCopy() {
+            List<PanelConfig> copiedList = new ArrayList<PanelConfig>();
+            for (PanelConfig panelConfig : mConfigMap.values()) {
+                copiedList.add(new PanelConfig(panelConfig));
+            }
+
+            return copiedList;
+        }
+
+        private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) {
+            if (panelConfig.isDisabled() == disabled) {
+                return;
+            }
+
+            panelConfig.setIsDisabled(disabled);
+            mEnabledCount += (disabled ? -1 : 1);
+        }
+
+        /**
+         * Gets the ID of the current default panel.
+         */
+        public String getDefaultPanelId() {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            if (mDefaultPanel == null) {
+                return null;
+            }
+
+            return mDefaultPanel.getId();
+        }
+
+        /**
+         * Set a new default panel.
+         *
+         * @param panelId the ID of the new default panel.
+         */
+        public void setDefault(String panelId) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            final PanelConfig panelConfig = getPanelOrThrow(panelId);
+            if (isCurrentDefaultPanel(panelConfig)) {
+                return;
+            }
+
+            if (mDefaultPanel != null) {
+                mDefaultPanel.setIsDefault(false);
+            }
+
+            panelConfig.setIsDefault(true);
+            setPanelIsDisabled(panelConfig, false);
+
+            mDefaultPanel = panelConfig;
+        }
+
+        /**
+         * Toggles disabled state for a panel.
+         *
+         * @param panelId the ID of the target panel.
+         * @param disabled true to disable the panel.
+         */
+        public void setDisabled(String panelId, boolean disabled) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            final PanelConfig panelConfig = getPanelOrThrow(panelId);
+            if (panelConfig.isDisabled() == disabled) {
+                return;
+            }
+
+            setPanelIsDisabled(panelConfig, disabled);
+
+            if (disabled) {
+                if (isCurrentDefaultPanel(panelConfig)) {
+                    panelConfig.setIsDefault(false);
+                    findNewDefault();
+                }
+            } else if (mEnabledCount == 1) {
+                setDefault(panelId);
+            }
+        }
+
+        /**
+         * Adds a new {@code PanelConfig}. It will do nothing if the
+         * {@code Editor} already contains a panel with the same ID.
+         *
+         * @param panelConfig the {@code PanelConfig} instance to be added.
+         * @return true if the item has been added.
+         */
+        public boolean install(PanelConfig panelConfig) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            if (panelConfig == null) {
+                throw new IllegalStateException("Can't install a null panel");
+            }
+
+            if (!panelConfig.isDynamic()) {
+                throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId());
+            }
+
+            if (panelConfig.isDisabled()) {
+                throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId());
+            }
+
+            boolean installed = false;
+
+            final String id = panelConfig.getId();
+            if (!mConfigMap.containsKey(id)) {
+                mConfigMap.put(id, panelConfig);
+
+                mEnabledCount++;
+                if (mEnabledCount == 1 || panelConfig.isDefault()) {
+                    setDefault(panelConfig.getId());
+                }
+
+                installed = true;
+            }
+
+            return installed;
+        }
+
+        /**
+         * Removes an existing panel.
+         *
+         * @return true if the item has been removed.
+         */
+        public boolean uninstall(String panelId) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            final PanelConfig panelConfig = mConfigMap.get(panelId);
+            if (panelConfig == null) {
+                return false;
+            }
+
+            if (!panelConfig.isDynamic()) {
+                throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId());
+            }
+
+            mConfigMap.remove(panelId);
+
+            if (!panelConfig.isDisabled()) {
+                mEnabledCount--;
+            }
+
+            if (isCurrentDefaultPanel(panelConfig)) {
+                findNewDefault();
+            }
+
+            return true;
+        }
+
+        /**
+         * Replaces an existing panel with a new {@code PanelConfig} instance.
+         *
+         * @return true if the item has been updated.
+         */
+        public boolean update(PanelConfig panelConfig) {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            if (panelConfig == null) {
+                throw new IllegalStateException("Can't update a null panel");
+            }
+
+            boolean updated = false;
+
+            final String id = panelConfig.getId();
+            if (mConfigMap.containsKey(id)) {
+                final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig);
+
+                // The disabled and default states can't never be
+                // changed by an update operation.
+                panelConfig.setIsDefault(oldPanelConfig.isDefault());
+                panelConfig.setIsDisabled(oldPanelConfig.isDisabled());
+
+                updated = true;
+            }
+
+            return updated;
+        }
+
+        /**
+         * Saves the current {@code Editor} state asynchronously in the
+         * background thread.
+         *
+         * @return the resulting {@code State} instance.
+         */
+        public State apply() {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            // We're about to save the current state in the background thread
+            // so we should use a deep copy of the PanelConfig instances to
+            // avoid saving corrupted state.
+            final State newConfigState = new State(mHomeConfig, makeDeepCopy());
+
+            ThreadUtils.getBackgroundHandler().post(new Runnable() {
+                @Override
+                public void run() {
+                    mHomeConfig.save(newConfigState);
+                }
+            });
+
+            return newConfigState;
+        }
+
+        /**
+         * Saves the current {@code Editor} state synchronously in the
+         * current thread.
+         *
+         * @return the resulting {@code State} instance.
+         */
+        public State commit() {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            final State newConfigState =
+                    new State(mHomeConfig, new ArrayList<PanelConfig>(mConfigMap.values()));
+
+            // This is a synchronous blocking operation, hence no
+            // need to deep copy the current PanelConfig instances.
+            mHomeConfig.save(newConfigState);
+
+            return newConfigState;
+        }
+
+        public boolean isEmpty() {
+            return mConfigMap.isEmpty();
+        }
+
+        @Override
+        public Iterator<PanelConfig> iterator() {
+            ThreadUtils.assertOnThread(mOriginalThread);
+
+            return mConfigMap.values().iterator();
+        }
+    }
+
     public interface OnChangeListener {
         public void onChange();
     }
 
     public interface HomeConfigBackend {
         public List<PanelConfig> load();
         public void save(List<PanelConfig> entries);
         public String getLocale();
@@ -710,26 +1083,27 @@ public final class HomeConfig {
     private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
 
     private final HomeConfigBackend mBackend;
 
     public HomeConfig(HomeConfigBackend backend) {
         mBackend = backend;
     }
 
-    public List<PanelConfig> load() {
-        return mBackend.load();
+    public State load() {
+        final List<PanelConfig> panelConfigs = mBackend.load();
+        return new State(this, panelConfigs);
     }
 
     public String getLocale() {
         return mBackend.getLocale();
     }
 
-    public void save(List<PanelConfig> panelConfigs) {
-        mBackend.save(panelConfigs);
+    public void save(State configState) {
+        mBackend.save(configState.mPanelConfigs);
     }
 
     public void setOnChangeListener(OnChangeListener listener) {
         mBackend.setOnChangeListener(listener);
     }
 
     public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) {
         return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class));
--- a/mobile/android/base/home/HomeConfigInvalidator.java
+++ b/mobile/android/base/home/HomeConfigInvalidator.java
@@ -185,141 +185,104 @@ public class HomeConfigInvalidator imple
         } else {
             handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC);
         }
 
         Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode);
     }
 
     /**
-     * Replace an element if a matching PanelConfig is
-     * present in the given list.
-     */
-    private boolean replacePanelConfig(List<PanelConfig> panelConfigs, PanelConfig panelConfig) {
-        final int index = panelConfigs.indexOf(panelConfig);
-        if (index >= 0) {
-            panelConfigs.set(index, panelConfig);
-            Log.d(LOGTAG, "executePendingChanges: replaced position " + index + " with " + panelConfig.getId());
-
-            return true;
-        }
-
-        return false;
-    }
-
-    private PanelConfig findPanelConfigWithId(List<PanelConfig> panelConfigs, String panelId) {
-        for (PanelConfig panelConfig : panelConfigs) {
-            if (panelConfig.getId().equals(panelId)) {
-                return panelConfig;
-            }
-        }
-
-        return null;
-    }
-
-    /**
      * Runs in the background thread.
      */
-    private List<PanelConfig> executePendingChanges(List<PanelConfig> panelConfigs) {
+    private void executePendingChanges(HomeConfig.Editor editor) {
         boolean shouldRefresh = false;
 
         while (!mPendingChanges.isEmpty()) {
             final ConfigChange pendingChange = mPendingChanges.poll();
 
             switch (pendingChange.type) {
                 case UNINSTALL: {
                     final String panelId = (String) pendingChange.target;
-                    final PanelConfig panelConfig = findPanelConfigWithId(panelConfigs, panelId);
-                    if (panelConfig != null && panelConfigs.remove(panelConfig)) {
-                        Log.d(LOGTAG, "executePendingChanges: removed panel " + panelConfig.getId());
+                    if (editor.uninstall(panelId)) {
+                        Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId);
                     }
                     break;
                 }
 
                 case INSTALL: {
                     final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
-                    if (!replacePanelConfig(panelConfigs, panelConfig)) {
-                        panelConfigs.add(panelConfig);
+                    if (editor.install(panelConfig)) {
                         Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId());
                     }
                     break;
                 }
 
                 case UPDATE: {
                     final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
-                    if (!replacePanelConfig(panelConfigs, panelConfig)) {
-                        Log.w(LOGTAG, "Tried to update non-existing panel " + panelConfig.getId());
+                    if (editor.update(panelConfig)) {
+                        Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId());
                     }
                     break;
                 }
 
                 case REFRESH: {
                     shouldRefresh = true;
                 }
             }
         }
 
         if (shouldRefresh) {
-            return executeRefresh(panelConfigs);
-        } else {
-            return panelConfigs;
+            executeRefresh(editor);
         }
     }
 
     /**
      * Runs in the background thread.
      */
-    private List<PanelConfig> refreshFromPanelInfos(List<PanelConfig> panelConfigs, List<PanelInfo> panelInfos) {
+    private void refreshFromPanelInfos(HomeConfig.Editor editor, List<PanelInfo> panelInfos) {
         Log.d(LOGTAG, "refreshFromPanelInfos");
 
-        final int count = panelConfigs.size();
-        for (int i = 0; i < count; i++) {
-            final PanelConfig panelConfig = panelConfigs.get(i);
+        for (PanelConfig panelConfig : editor) {
+            PanelConfig refreshedPanelConfig = null;
 
-            PanelConfig refreshedPanelConfig = null;
             if (panelConfig.isDynamic()) {
                 for (PanelInfo panelInfo : panelInfos) {
                     if (panelInfo.getId().equals(panelConfig.getId())) {
                         refreshedPanelConfig = panelInfo.toPanelConfig();
                         Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId());
                         break;
                     }
                 }
             } else {
                 refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType());
                 Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId());
             }
 
             if (refreshedPanelConfig == null) {
                 Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId());
-                refreshedPanelConfig = panelConfig;
+                continue;
             }
 
-            refreshedPanelConfig.setIsDefault(panelConfig.isDefault());
-            refreshedPanelConfig.setIsDisabled(panelConfig.isDisabled());
-
-            Log.d(LOGTAG, "refreshFromPanelInfos: set " + i + " with " + refreshedPanelConfig.getId());
-            panelConfigs.set(i, refreshedPanelConfig);
+            Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId());
+            editor.update(refreshedPanelConfig);
         }
-
-        return panelConfigs;
     }
 
     /**
      * Runs in the background thread.
      */
-    private List<PanelConfig> executeRefresh(List<PanelConfig> panelConfigs) {
-        if (panelConfigs.isEmpty()) {
-            return panelConfigs;
+    private void executeRefresh(HomeConfig.Editor editor) {
+        if (editor.isEmpty()) {
+            return;
         }
 
         Log.d(LOGTAG, "executeRefresh");
 
         final Set<String> ids = new HashSet<String>();
-        for (PanelConfig panelConfig : panelConfigs) {
+        for (PanelConfig panelConfig : editor) {
             ids.add(panelConfig.getId());
         }
 
         final Object panelRequestLock = new Object();
         final List<PanelInfo> latestPanelInfos = new ArrayList<PanelInfo>();
 
         final PanelManager pm = new PanelManager();
         pm.requestPanelsById(ids, new RequestCallback() {
@@ -334,26 +297,27 @@ public class HomeConfigInvalidator imple
             }
         });
 
         try {
             synchronized(panelRequestLock) {
                 panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC);
 
                 Log.d(LOGTAG, "executeRefresh: done fetching panel infos");
-                return refreshFromPanelInfos(panelConfigs, latestPanelInfos);
+                refreshFromPanelInfos(editor, latestPanelInfos);
             }
         } catch (InterruptedException e) {
             Log.e(LOGTAG, "Failed to fetch panels from gecko", e);
-            return panelConfigs;
         }
     }
 
     /**
      * Runs in the background thread.
      */
     private class InvalidationRunnable implements Runnable {
         @Override
         public void run() {
-            mHomeConfig.save(executePendingChanges(mHomeConfig.load()));
+            final HomeConfig.Editor editor = mHomeConfig.load().edit();
+            executePendingChanges(editor);
+            editor.commit();
         }
     };
 }
--- a/mobile/android/base/home/HomeConfigLoader.java
+++ b/mobile/android/base/home/HomeConfigLoader.java
@@ -8,74 +8,74 @@ package org.mozilla.gecko.home;
 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
 import org.mozilla.gecko.home.HomeConfig.OnChangeListener;
 
 import android.content.Context;
 import android.support.v4.content.AsyncTaskLoader;
 
 import java.util.List;
 
-public class HomeConfigLoader extends AsyncTaskLoader<List<PanelConfig>> {
+public class HomeConfigLoader extends AsyncTaskLoader<HomeConfig.State> {
     private final HomeConfig mConfig;
-    private List<PanelConfig> mPanelConfigs;
+    private HomeConfig.State mConfigState;
 
     public HomeConfigLoader(Context context, HomeConfig homeConfig) {
         super(context);
         mConfig = homeConfig;
     }
 
     @Override
-    public List<PanelConfig> loadInBackground() {
+    public HomeConfig.State loadInBackground() {
         return mConfig.load();
     }
 
     @Override
-    public void deliverResult(List<PanelConfig> panelConfigs) {
+    public void deliverResult(HomeConfig.State configState) {
         if (isReset()) {
-            mPanelConfigs = null;
+            mConfigState = null;
             return;
         }
 
-        mPanelConfigs = panelConfigs;
+        mConfigState = configState;
         mConfig.setOnChangeListener(new ForceLoadChangeListener());
 
         if (isStarted()) {
-            super.deliverResult(panelConfigs);
+            super.deliverResult(configState);
         }
     }
 
     @Override
     protected void onStartLoading() {
-        if (mPanelConfigs != null) {
-            deliverResult(mPanelConfigs);
+        if (mConfigState != null) {
+            deliverResult(mConfigState);
         }
 
-        if (takeContentChanged() || mPanelConfigs == null) {
+        if (takeContentChanged() || mConfigState == null) {
             forceLoad();
         }
     }
 
     @Override
     protected void onStopLoading() {
         cancelLoad();
     }
 
     @Override
-    public void onCanceled(List<PanelConfig> panelConfigs) {
-        mPanelConfigs = null;
+    public void onCanceled(HomeConfig.State configState) {
+        mConfigState = null;
     }
 
     @Override
     protected void onReset() {
         super.onReset();
 
         // Ensure the loader is stopped.
         onStopLoading();
 
-        mPanelConfigs = null;
+        mConfigState = null;
         mConfig.setOnChangeListener(null);
     }
 
     private class ForceLoadChangeListener implements OnChangeListener {
         @Override
         public void onChange() {
             onContentChanged();
         }
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -245,17 +245,17 @@ public class HomePager extends ViewPager
     }
 
     public void onToolbarFocusChange(boolean hasFocus) {
         // We should only make the banner active if the toolbar is not focused and we are on the default page
         final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex;
         mHomeBanner.setActive(active);
     }
 
-    private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) {
+    private void updateUiFromConfigState(HomeConfig.State configState) {
         // We only care about the adapter if HomePager is currently
         // loaded, which means it's visible in the activity.
         if (!mLoaded) {
             return;
         }
 
         if (mDecor != null) {
             mDecor.removeAllPagerViews();
@@ -265,17 +265,17 @@ public class HomePager extends ViewPager
 
         // Destroy any existing panels currently loaded
         // in the pager.
         setAdapter(null);
 
         // Only keep enabled panels.
         final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>();
 
-        for (PanelConfig panelConfig : panelConfigs) {
+        for (PanelConfig panelConfig : configState) {
             if (!panelConfig.isDisabled()) {
                 enabledPanels.add(panelConfig);
             }
         }
 
         // Update the adapter with the new panel configs
         adapter.update(enabledPanels);
 
@@ -309,29 +309,29 @@ public class HomePager extends ViewPager
                     mDefaultPageIndex = i;
                     setCurrentItem(i, false);
                     break;
                 }
             }
         }
     }
 
-    private class ConfigLoaderCallbacks implements LoaderCallbacks<List<PanelConfig>> {
+    private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
         @Override
-        public Loader<List<PanelConfig>> onCreateLoader(int id, Bundle args) {
+        public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
             return new HomeConfigLoader(mContext, mConfig);
         }
 
         @Override
-        public void onLoadFinished(Loader<List<PanelConfig>> loader, List<PanelConfig> panelConfigs) {
-            updateUiFromPanelConfigs(panelConfigs);
+        public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
+            updateUiFromConfigState(configState);
         }
 
         @Override
-        public void onLoaderReset(Loader<List<PanelConfig>> loader) {
+        public void onLoaderReset(Loader<HomeConfig.State> loader) {
         }
     }
 
     private class PageChangeListener implements ViewPager.OnPageChangeListener {
         @Override
         public void onPageSelected(int position) {
             if (mDecor != null) {
                 mDecor.onPageSelected(position);
--- a/mobile/android/base/home/HomePanelPicker.java
+++ b/mobile/android/base/home/HomePanelPicker.java
@@ -219,29 +219,29 @@ public class HomePanelPicker extends Fra
             mPanelInfos = panelInfos;
             notifyDataSetChanged();
         }
     }
 
     /**
      * Fetch installed Home panels and update the adapter for this activity.
      */
-    private class ConfigLoaderCallbacks implements LoaderCallbacks<List<PanelConfig>> {
+    private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
         @Override
-        public Loader<List<PanelConfig>> onCreateLoader(int id, Bundle args) {
+        public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
             final HomeConfig homeConfig = HomeConfig.getDefault(HomePanelPicker.this);
             return new HomeConfigLoader(HomePanelPicker.this, homeConfig);
         }
 
         @Override
-        public void onLoadFinished(Loader<List<PanelConfig>> loader, List<PanelConfig> panelConfigs) {
+        public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
             mCurrentPanelsIds = new ArrayList<String>();
-            for (PanelConfig panelConfig : panelConfigs) {
+            for (PanelConfig panelConfig : configState) {
                 mCurrentPanelsIds.add(panelConfig.getId());
             }
 
             updatePanelsAdapter(mPanelInfos);
         }
 
         @Override
-        public void onLoaderReset(Loader<List<PanelConfig>> loader) {}
+        public void onLoaderReset(Loader<HomeConfig.State> loader) {}
     }
 }
--- a/mobile/android/base/preferences/PanelsPreferenceCategory.java
+++ b/mobile/android/base/preferences/PanelsPreferenceCategory.java
@@ -15,20 +15,19 @@ import org.mozilla.gecko.util.UiAsyncTas
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 
 public class PanelsPreferenceCategory extends CustomListCategory {
     public static final String LOGTAG = "PanelsPrefCategory";
 
     protected HomeConfig mHomeConfig;
-    protected List<PanelConfig> mPanelConfigs;
+    protected HomeConfig.Editor mConfigEditor;
 
-    protected UiAsyncTask<Void, Void, List<PanelConfig>> mLoadTask;
-    protected UiAsyncTask<Void, Void, Void> mSaveTask;
+    protected UiAsyncTask<Void, Void, HomeConfig.State> mLoadTask;
 
     public PanelsPreferenceCategory(Context context) {
         super(context);
         initConfig(context);
     }
 
     public PanelsPreferenceCategory(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -50,193 +49,114 @@ public class PanelsPreferenceCategory ex
 
         loadHomeConfig();
     }
 
     /**
      * Load the Home Panels config and populate the preferences screen and maintain local state.
      */
     private void loadHomeConfig() {
-        mLoadTask = new UiAsyncTask<Void, Void, List<PanelConfig>>(ThreadUtils.getBackgroundHandler()) {
+        mLoadTask = new UiAsyncTask<Void, Void, HomeConfig.State>(ThreadUtils.getBackgroundHandler()) {
             @Override
-            public List<PanelConfig> doInBackground(Void... params) {
+            public HomeConfig.State doInBackground(Void... params) {
                 return mHomeConfig.load();
             }
 
             @Override
-            public void onPostExecute(List<PanelConfig> panelConfigs) {
-                mPanelConfigs = panelConfigs;
-                displayHomeConfig();
+            public void onPostExecute(HomeConfig.State configState) {
+                mConfigEditor = configState.edit();
+                displayHomeConfig(configState);
             }
         };
         mLoadTask.execute();
     }
 
-    private void displayHomeConfig() {
-        for (PanelConfig panelConfig : mPanelConfigs) {
+    private void displayHomeConfig(HomeConfig.State configState) {
+        for (PanelConfig panelConfig : configState) {
             // Create and add the pref.
             final PanelsPreference pref = new PanelsPreference(getContext(), PanelsPreferenceCategory.this);
             pref.setTitle(panelConfig.getTitle());
             pref.setKey(panelConfig.getId());
             // XXX: Pull icon from PanelInfo.
             addPreference(pref);
 
-            if (panelConfig.isDefault()) {
-                mDefaultReference = pref;
-                pref.setIsDefault(true);
-            }
-
             if (panelConfig.isDisabled()) {
                 pref.setHidden(true);
             }
         }
+
+        setDefaultFromConfig();
     }
 
-    /**
-     * Update HomeConfig off the main thread.
-     *
-     * @param panelConfigs Configuration to be saved
-     */
-    private void saveHomeConfig() {
-        if (mPanelConfigs == null) {
+    private void setDefaultFromConfig() {
+        final String defaultPanelId = mConfigEditor.getDefaultPanelId();
+        if (defaultPanelId == null) {
+            mDefaultReference = null;
             return;
         }
 
-        final List<PanelConfig> panelConfigs = makeConfigListDeepCopy();
-        mSaveTask = new UiAsyncTask<Void, Void, Void>(ThreadUtils.getBackgroundHandler()) {
-            @Override
-            public Void doInBackground(Void... params) {
-                mHomeConfig.save(panelConfigs);
-                return null;
+        final int prefCount = getPreferenceCount();
+
+        // First preference (index 0) is Preference to add panels.
+        for (int i = 1; i < prefCount; i++) {
+            final PanelsPreference pref = (PanelsPreference) getPreference(i);
+
+            if (defaultPanelId.equals(pref.getKey())) {
+                super.setDefault(pref);
+                break;
             }
-        };
-        mSaveTask.execute();
-    }
-
-    private List<PanelConfig> makeConfigListDeepCopy() {
-        List<PanelConfig> copiedList = new ArrayList<PanelConfig>();
-        for (PanelConfig panelConfig : mPanelConfigs) {
-            copiedList.add(new PanelConfig(panelConfig));
         }
-        return copiedList;
     }
 
     @Override
     public void setDefault(CustomListPreference pref) {
         super.setDefault(pref);
-        updateConfigDefault();
-        saveHomeConfig();
+
+        final String id = pref.getKey();
+
+        final String defaultPanelId = mConfigEditor.getDefaultPanelId();
+        if (defaultPanelId != null && defaultPanelId.equals(id)) {
+            return;
+        }
+
+        mConfigEditor.setDefault(id);
+        mConfigEditor.apply();
     }
 
     @Override
     protected void onPrepareForRemoval() {
         if (mLoadTask != null) {
             mLoadTask.cancel(true);
         }
-
-        if (mSaveTask != null) {
-            mSaveTask.cancel(true);
-        }
-     }
-
-    /**
-     * Update the local HomeConfig default state from mDefaultReference.
-     */
-    private void updateConfigDefault() {
-        String id = null;
-        if (mDefaultReference != null) {
-            id = mDefaultReference.getKey();
-        }
-
-        for (PanelConfig panelConfig : mPanelConfigs) {
-            if (TextUtils.equals(panelConfig.getId(), id)) {
-                panelConfig.setIsDefault(true);
-                panelConfig.setIsDisabled(false);
-            } else {
-                panelConfig.setIsDefault(false);
-            }
-        }
     }
 
     @Override
     public void uninstall(CustomListPreference pref) {
-        super.uninstall(pref);
-        // This could change the default, so update the local version of the config.
-        updateConfigDefault();
+        mConfigEditor.uninstall(pref.getKey());
+        mConfigEditor.apply();
 
-        final String id = pref.getKey();
-        PanelConfig toRemove = null;
-        for (PanelConfig panelConfig : mPanelConfigs) {
-            if (TextUtils.equals(panelConfig.getId(), id)) {
-                toRemove = panelConfig;
-                break;
-            }
-        }
-        mPanelConfigs.remove(toRemove);
-
-        saveHomeConfig();
+        super.uninstall(pref);
     }
 
     /**
      * Update the hide/show state of the preference and save the HomeConfig
      * changes.
      *
      * @param pref Preference to update
      * @param toHide New hidden state of the preference
      */
     protected void setHidden(PanelsPreference pref, boolean toHide) {
-        pref.setHidden(toHide);
-        ensureDefaultForHide(pref, toHide);
-
-        final String id = pref.getKey();
-        for (PanelConfig panelConfig : mPanelConfigs) {
-            if (TextUtils.equals(panelConfig.getId(), id)) {
-                panelConfig.setIsDisabled(toHide);
-                break;
-            }
-        }
-
-        saveHomeConfig();
-    }
+        mConfigEditor.setDisabled(pref.getKey(), toHide);
+        mConfigEditor.apply();
 
-    /**
-     * Ensure a default is set (if possible) for hiding/showing a pref.
-     * If hiding, try to find an enabled pref to set as the default.
-     * If showing, set it as the default if there is no default currently.
-     *
-     * This updates the local HomeConfig state.
-     *
-     * @param pref Preference getting updated
-     * @param toHide Boolean of the new hidden state
-     */
-    private void ensureDefaultForHide(PanelsPreference pref, boolean toHide) {
-        if (toHide) {
-            // Set a default if there is an enabled panel left.
-            if (pref == mDefaultReference) {
-                setFallbackDefault();
-                updateConfigDefault();
-            }
-        } else {
-            if (mDefaultReference == null) {
-                super.setDefault(pref);
-                updateConfigDefault();
-            }
-        }
+        pref.setHidden(toHide);
+        setDefaultFromConfig();
     }
 
     /**
      * When the default panel is removed or disabled, find an enabled panel
      * if possible and set it as mDefaultReference.
      */
     @Override
     protected void setFallbackDefault() {
-        // First preference (index 0) is Preference to add panels.
-        final int prefsCount = getPreferenceCount();
-        for (int i = 1; i < prefsCount; i++) {
-            final PanelsPreference pref = (PanelsPreference) getPreference(i);
-            if (!pref.isHidden()) {
-                super.setDefault(pref);
-                return;
-            }
-        }
-        mDefaultReference = null;
+        setDefaultFromConfig();
     }
 }