Bug 967742 - Consolidate API to edit HomeConfig state (r=margaret)
authorLucas Rocha <lucasr@mozilla.com>
Fri, 07 Mar 2014 17:15:31 +0000
changeset 172493 d184b2d30734084cce49c65a0e3693b49b12edc2
parent 172492 cc50c0d103cbc1be642f438ddbfea1793e2eb623
child 172494 51f11b099596c5ade911975fe9135e52ad9e355e
push id5532
push userlrocha@mozilla.com
push dateFri, 07 Mar 2014 17:16:00 +0000
treeherderfx-team@d184b2d30734 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs967742
milestone30.0a1
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();
     }
 }