Bug 942231 - Change HomePager to be backed by HomeConfig (r=margaret)
authorLucas Rocha <lucasr@mozilla.com>
Thu, 12 Dec 2013 12:04:31 +0000
changeset 160262 fe13b62902c8070f4bd59bf0b42f7bd9936c2eaf
parent 160261 ce3dd14c1840cd81593dd4eba22e4659719b4769
child 160263 adb909c5d6c3edcd0c5482815a4558f0839a4184
push id37540
push userkwierso@gmail.com
push dateFri, 13 Dec 2013 03:23:05 +0000
treeherdermozilla-inbound@1bc33fa19b24 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs942231
milestone29.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 942231 - Change HomePager to be backed by HomeConfig (r=margaret)
CLOBBER
mobile/android/base/BrowserApp.java
mobile/android/base/Tab.java
mobile/android/base/home/HomeAdapter.java
mobile/android/base/home/HomeConfig.java
mobile/android/base/home/HomeConfigLoader.java
mobile/android/base/home/HomeConfigMemBackend.java
mobile/android/base/home/HomePager.java
mobile/android/base/home/ListPage.java
mobile/android/base/moz.build
--- a/CLOBBER
+++ b/CLOBBER
@@ -13,9 +13,9 @@
 #          |               |
 #          O <-- Clobber   O  <-- Clobber
 #
 # Note: The description below will be part of the error message shown to users.
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
-Bug 946067 required a clobber on Windows because bug 928195
+Bug 942231 needs a clobber -- JNI wrappers need to be re-generated.
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1462,17 +1462,17 @@ abstract public class BrowserApp extends
 
         final Tab selectedTab = Tabs.getInstance().getSelectedTab();
         mTargetTabForEditingMode = (selectedTab != null ? selectedTab.getId() : null);
 
         final PropertyAnimator animator = new PropertyAnimator(250);
         animator.setUseHardwareLayer(false);
 
         mBrowserToolbar.startEditing(url, animator);
-        showHomePagerWithAnimator(HomePager.Page.TOP_SITES, animator);
+        showHomePagerWithAnimator(animator);
 
         animator.start();
     }
 
     private void commitEditingMode() {
         if (!mBrowserToolbar.isEditing()) {
             return;
         }
@@ -1618,29 +1618,35 @@ abstract public class BrowserApp extends
         }
     }
 
     @Override
     public void onLocaleReady(final String locale) {
         super.onLocaleReady(locale);
         if (mHomePager != null) {
             // Blow it away and rebuild it with the right strings.
-            mHomePager.redisplay(getSupportFragmentManager());
+            mHomePager.redisplay(getSupportLoaderManager(), getSupportFragmentManager());
         }
 
         if (mMenu != null) {
             mMenu.clear();
             onCreateOptionsMenu(mMenu);
         }
     }
 
     private void showHomePager(HomePager.Page page) {
         showHomePagerWithAnimator(page, null);
     }
 
+    private void showHomePagerWithAnimator(PropertyAnimator animator) {
+        // Passing null here means the default page will be defined
+        // by the HomePager's configuration.
+        showHomePagerWithAnimator(null, animator);
+    }
+
     private void showHomePagerWithAnimator(HomePager.Page page, PropertyAnimator animator) {
         if (isHomePagerVisible()) {
             return;
         }
 
         // Refresh toolbar height to possibly restore the toolbar padding
         refreshToolbarHeight();
 
@@ -1650,17 +1656,19 @@ abstract public class BrowserApp extends
             mLayerView.getLayerMarginsAnimator().showMargins(true);
         }
 
         if (mHomePager == null) {
             final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
             mHomePager = (HomePager) homePagerStub.inflate();
         }
 
-        mHomePager.show(getSupportFragmentManager(), page, animator);
+        mHomePager.show(getSupportLoaderManager(),
+                        getSupportFragmentManager(),
+                        page, animator);
 
         // Hide the web content so it cannot be focused by screen readers.
         hideWebContentOnPropertyAnimationEnd(animator);
     }
 
     private void hideWebContentOnPropertyAnimationEnd(final PropertyAnimator animator) {
         if (animator == null) {
             hideWebContent();
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -90,17 +90,17 @@ public class Tab {
         mAppContext = context.getApplicationContext();
         mId = id;
         mLastUsed = 0;
         mUrl = url;
         mBaseDomain = "";
         mUserSearch = "";
         mExternal = external;
         mParentId = parentId;
-        mAboutHomePage = HomePager.Page.TOP_SITES;
+        mAboutHomePage = null;
         mTitle = title == null ? "" : title;
         mFavicon = null;
         mFaviconUrl = null;
         mFaviconSize = 0;
         mHasFeeds = false;
         mHasOpenSearch = false;
         mSiteIdentity = new SiteIdentity();
         mReaderEnabled = false;
@@ -150,17 +150,16 @@ public class Tab {
     public HomePager.Page getAboutHomePage() {
         return mAboutHomePage;
     }
 
     private void setAboutHomePage(HomePager.Page page) {
         mAboutHomePage = page;
     }
 
-
     // may be null if user-entered query hasn't yet been resolved to a URI
     public synchronized String getURL() {
         return mUrl;
     }
 
     // mUserSearch should never be null, but it may be an empty string
     public synchronized String getUserSearch() {
         return mUserSearch;
@@ -655,16 +654,18 @@ public class Tab {
         setZoomConstraints(new ZoomConstraints(true));
         setHasTouchListeners(false);
         setBackgroundColor(DEFAULT_BACKGROUND_COLOR);
         setErrorType(ErrorType.NONE);
 
         final String homePage = message.getString("aboutHomePage");
         if (!TextUtils.isEmpty(homePage)) {
             setAboutHomePage(HomePager.Page.valueOf(homePage));
+        } else {
+            setAboutHomePage(null);
         }
 
         Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
     }
 
     private static boolean shouldShowProgress(final String url) {
         return AboutPages.isAboutHome(url) ||
                AboutPages.isAboutReader(url);
--- a/mobile/android/base/home/HomeAdapter.java
+++ b/mobile/android/base/home/HomeAdapter.java
@@ -1,139 +1,185 @@
 /* -*- 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.home.HomeConfig.PageEntry;
+import org.mozilla.gecko.home.HomeConfig.PageType;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.HomePager.Page;
 
 import android.content.Context;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentStatePagerAdapter;
 import android.view.ViewGroup;
 
 import java.util.ArrayList;
-import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
 
 class HomeAdapter extends FragmentStatePagerAdapter {
 
     private final Context mContext;
     private final ArrayList<PageInfo> mPageInfos;
-    private final EnumMap<Page, Fragment> mPages;
+    private final HashMap<String, Fragment> mPages;
+
+    private boolean mCanLoadHint;
 
     private OnAddPageListener mAddPageListener;
 
     interface OnAddPageListener {
         public void onAddPage(String title);
     }
 
-    final class PageInfo {
-        private final Page page;
-        private final Class<?> clss;
-        private final Bundle args;
-        private final String title;
-
-        PageInfo(Page page, Class<?> clss, Bundle args, String title) {
-            this.page = page;
-            this.clss = clss;
-            this.args = args;
-            this.title = title;
-        }
-    }
-
     public HomeAdapter(Context context, FragmentManager fm) {
         super(fm);
 
         mContext = context;
+        mCanLoadHint = HomeFragment.DEFAULT_CAN_LOAD_HINT;
 
         mPageInfos = new ArrayList<PageInfo>();
-        mPages = new EnumMap<Page, Fragment>(Page.class);
+        mPages = new HashMap<String, Fragment>();
     }
 
     @Override
     public int getCount() {
         return mPageInfos.size();
     }
 
     @Override
     public Fragment getItem(int position) {
         PageInfo info = mPageInfos.get(position);
-        return Fragment.instantiate(mContext, info.clss.getName(), info.args);
+        return Fragment.instantiate(mContext, info.getClassName(), info.getArgs());
     }
 
     @Override
     public CharSequence getPageTitle(int position) {
-        PageInfo info = mPageInfos.get(position);
-        return info.title.toUpperCase();
+        if (mPageInfos.size() > 0) {
+            PageInfo info = mPageInfos.get(position);
+            return info.getTitle().toUpperCase();
+        }
+
+        return null;
     }
 
     @Override
     public Object instantiateItem(ViewGroup container, int position) {
         Fragment fragment = (Fragment) super.instantiateItem(container, position);
-        mPages.put(mPageInfos.get(position).page, fragment);
+        mPages.put(mPageInfos.get(position).getId(), fragment);
 
         return fragment;
     }
 
     @Override
     public void destroyItem(ViewGroup container, int position, Object object) {
         super.destroyItem(container, position, object);
-        mPages.remove(mPageInfos.get(position).page);
+        mPages.remove(mPageInfos.get(position).getId());
     }
 
     public void setOnAddPageListener(OnAddPageListener listener) {
         mAddPageListener = listener;
     }
 
-    public void addPage(Page page, Class<?> clss, Bundle args, String title) {
-        addPage(-1, page, clss, args, title);
-    }
-
-    public void addPage(int index, Page page, Class<?> clss, Bundle args, String title) {
-        PageInfo info = new PageInfo(page, clss, args, title);
-
-        if (index >= 0) {
-            mPageInfos.add(index, info);
-        } else {
-            mPageInfos.add(info);
-        }
-
-        notifyDataSetChanged();
-
-        if (mAddPageListener != null) {
-            mAddPageListener.onAddPage(title);
-        }
-    }
-
     public int getItemPosition(Page page) {
         for (int i = 0; i < mPageInfos.size(); i++) {
-            PageInfo info = mPageInfos.get(i);
-            if (info.page == page) {
+            final Page infoPage = mPageInfos.get(i).toPage();
+            if (infoPage == page) {
                 return i;
             }
         }
 
         return -1;
     }
 
     public Page getPageAtPosition(int position) {
         PageInfo info = mPageInfos.get(position);
-        return info.page;
+        return info.toPage();
+    }
+
+    private void addPage(PageInfo info) {
+        mPageInfos.add(info);
+
+        if (mAddPageListener != null) {
+            mAddPageListener.onAddPage(info.getTitle());
+        }
+    }
+
+    public void update(List<PageEntry> pageEntries) {
+        mPages.clear();
+        mPageInfos.clear();
+
+        if (pageEntries != null) {
+            for (PageEntry pageEntry : pageEntries) {
+                final PageInfo info = new PageInfo(pageEntry);
+                addPage(info);
+            }
+        }
+
+        notifyDataSetChanged();
+    }
+
+    public boolean getCanLoadHint() {
+        return mCanLoadHint;
     }
 
     public void setCanLoadHint(boolean canLoadHint) {
-        // Update fragment arguments for future instances
-        for (PageInfo info : mPageInfos) {
-            info.args.putBoolean(HomePager.CAN_LOAD_ARG, canLoadHint);
-        }
+        // We cache the last hint value so that we can use it when
+        // creating new pages. See PageInfo.getArgs().
+        mCanLoadHint = canLoadHint;
 
         // Enable/disable loading on all existing pages
         for (Fragment page : mPages.values()) {
             final HomeFragment homePage = (HomeFragment) page;
             homePage.setCanLoadHint(canLoadHint);
         }
     }
+
+    private final class PageInfo {
+        private final String mId;
+        private final PageEntry mPageEntry;
+
+        PageInfo(PageEntry pageEntry) {
+            mId = pageEntry.getType() + "-" + pageEntry.getId();
+            mPageEntry = pageEntry;
+        }
+
+        public String getId() {
+            return mId;
+        }
+
+        public String getTitle() {
+            return mPageEntry.getTitle();
+        }
+
+        public String getClassName() {
+            final PageType type = mPageEntry.getType();
+            return type.getPageClass().getName();
+        }
+
+        public Bundle getArgs() {
+            final Bundle args = new Bundle();
+
+            args.putBoolean(HomePager.CAN_LOAD_ARG, mCanLoadHint);
+
+            // Only list pages need the page entry
+            if (mPageEntry.getType() == PageType.LIST) {
+                args.putParcelable(HomePager.PAGE_ENTRY_ARG, mPageEntry);
+            }
+
+            return args;
+        }
+
+        public Page toPage() {
+            final PageType type = mPageEntry.getType();
+            if (type == PageType.LIST) {
+                return null;
+            }
+
+            return Page.valueOf(type);
+        }
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/HomeConfig.java
@@ -0,0 +1,224 @@
+/* -*- 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.home.HomePager.Page;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.EnumSet;
+import java.util.List;
+
+final class HomeConfig {
+    public static enum PageType implements Parcelable {
+        TOP_SITES("top_sites", TopSitesPage.class),
+        BOOKMARKS("bookmarks", BookmarksPage.class),
+        HISTORY("history", HistoryPage.class),
+        READING_LIST("reading_list", ReadingListPage.class),
+        LIST("list", ListPage.class);
+
+        private final String mId;
+        private final Class<?> mPageClass;
+
+        PageType(String id, Class<?> pageClass) {
+            mId = id;
+            mPageClass = pageClass;
+        }
+
+        public static PageType valueOf(Page page) {
+            switch(page) {
+                case TOP_SITES:
+                    return PageType.TOP_SITES;
+
+                case BOOKMARKS:
+                    return PageType.BOOKMARKS;
+
+                case HISTORY:
+                    return PageType.HISTORY;
+
+                case READING_LIST:
+                    return PageType.READING_LIST;
+
+                default:
+                    throw new IllegalArgumentException("Could not convert unrecognized Page");
+            }
+        }
+
+        public static PageType fromId(String id) {
+            if (id == null) {
+                throw new IllegalArgumentException("Could not convert null String to PageType");
+            }
+
+            for (PageType page : PageType.values()) {
+                if (TextUtils.equals(page.mId, id.toLowerCase())) {
+                    return page;
+                }
+            }
+
+            throw new IllegalArgumentException("Could not convert String id to PageType");
+        }
+
+        @Override
+        public String toString() {
+            return mId;
+        }
+
+        public Class<?> getPageClass() {
+            return mPageClass;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(ordinal());
+        }
+
+        public static final Creator<PageType> CREATOR = new Creator<PageType>() {
+            @Override
+            public PageType createFromParcel(final Parcel source) {
+                return PageType.values()[source.readInt()];
+            }
+
+            @Override
+            public PageType[] newArray(final int size) {
+                return new PageType[size];
+            }
+        };
+    }
+
+    public static class PageEntry implements Parcelable {
+        private final PageType mType;
+        private final String mTitle;
+        private final String mId;
+        private final EnumSet<Flags> mFlags;
+
+        public enum Flags {
+            DEFAULT_PAGE
+        }
+
+        @SuppressWarnings("unchecked")
+        public PageEntry(Parcel in) {
+            mType = (PageType) in.readParcelable(getClass().getClassLoader());
+            mTitle = in.readString();
+            mId = in.readString();
+            mFlags = (EnumSet<Flags>) in.readSerializable();
+        }
+
+        public PageEntry(PageType type, String title) {
+            this(type, title, EnumSet.noneOf(Flags.class));
+        }
+
+        public PageEntry(PageType type, String title, EnumSet<Flags> flags) {
+            this(type, title, type.toString(), flags);
+        }
+
+        public PageEntry(PageType type, String title, String id) {
+            this(type, title, id, EnumSet.noneOf(Flags.class));
+        }
+
+        public PageEntry(PageType type, String title, String id, EnumSet<Flags> flags) {
+            if (type == null) {
+                throw new IllegalArgumentException("Can't create PageEntry with null type");
+            }
+            mType = type;
+
+            if (title == null) {
+                throw new IllegalArgumentException("Can't create PageEntry with null title");
+            }
+            mTitle = title;
+
+            if (id == null) {
+                throw new IllegalArgumentException("Can't create PageEntry with null id");
+            }
+            mId = id;
+
+            if (flags == null) {
+                throw new IllegalArgumentException("Can't create PageEntry with null flags");
+            }
+            mFlags = flags;
+        }
+
+        public PageType getType() {
+            return mType;
+        }
+
+        public String getTitle() {
+            return mTitle;
+        }
+
+        public String getId() {
+            return mId;
+        }
+
+        public boolean isDefault() {
+            return mFlags.contains(Flags.DEFAULT_PAGE);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeParcelable(mType, 0);
+            dest.writeString(mTitle);
+            dest.writeString(mId);
+            dest.writeSerializable(mFlags);
+        }
+
+        public static final Creator<PageEntry> CREATOR = new Creator<PageEntry>() {
+            @Override
+            public PageEntry createFromParcel(final Parcel in) {
+                return new PageEntry(in);
+            }
+
+            @Override
+            public PageEntry[] newArray(final int size) {
+                return new PageEntry[size];
+            }
+        };
+    }
+
+    public interface OnChangeListener {
+        public void onChange();
+    }
+
+    public interface HomeConfigBackend {
+        public List<PageEntry> load();
+        public void save(List<PageEntry> entries);
+        public void setOnChangeListener(OnChangeListener listener);
+    }
+
+    private final HomeConfigBackend mBackend;
+
+    public HomeConfig(HomeConfigBackend backend) {
+        mBackend = backend;
+    }
+
+    public List<PageEntry> load() {
+        return mBackend.load();
+    }
+
+    public void save(List<PageEntry> entries) {
+        mBackend.save(entries);
+    }
+
+    public void setOnChangeListener(OnChangeListener listener) {
+        mBackend.setOnChangeListener(listener);
+    }
+
+    public static HomeConfig getDefault(Context context) {
+        return new HomeConfig(new HomeConfigMemBackend(context));
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/HomeConfigLoader.java
@@ -0,0 +1,83 @@
+/* -*- 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.home.HomeConfig.PageEntry;
+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<PageEntry>> {
+    private final HomeConfig mConfig;
+    private List<PageEntry> mPageEntries;
+
+    public HomeConfigLoader(Context context, HomeConfig homeConfig) {
+        super(context);
+        mConfig = homeConfig;
+    }
+
+    @Override
+    public List<PageEntry> loadInBackground() {
+        return mConfig.load();
+    }
+
+    @Override
+    public void deliverResult(List<PageEntry> pageEntries) {
+        if (isReset()) {
+            mPageEntries = null;
+            return;
+        }
+
+        mPageEntries = pageEntries;
+        mConfig.setOnChangeListener(new ForceLoadChangeListener());
+
+        if (isStarted()) {
+            super.deliverResult(pageEntries);
+        }
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mPageEntries != null) {
+            deliverResult(mPageEntries);
+        }
+
+        if (takeContentChanged() || mPageEntries == null) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        cancelLoad();
+    }
+
+    @Override
+    public void onCanceled(List<PageEntry> pageEntries) {
+        mPageEntries = null;
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+
+        // Ensure the loader is stopped.
+        onStopLoading();
+
+        mPageEntries = null;
+        mConfig.setOnChangeListener(null);
+    }
+
+    private class ForceLoadChangeListener implements OnChangeListener {
+        @Override
+        public void onChange() {
+            onContentChanged();
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/HomeConfigMemBackend.java
@@ -0,0 +1,67 @@
+/* -*- 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.home.HomeConfig.HomeConfigBackend;
+import org.mozilla.gecko.home.HomeConfig.OnChangeListener;
+import org.mozilla.gecko.home.HomeConfig.PageEntry;
+import org.mozilla.gecko.home.HomeConfig.PageType;
+import org.mozilla.gecko.util.HardwareUtils;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+
+class HomeConfigMemBackend implements HomeConfigBackend {
+    private final Context mContext;
+
+    public HomeConfigMemBackend(Context context) {
+        mContext = context;
+    }
+
+    public List<PageEntry> load() {
+        final ArrayList<PageEntry> pageEntries = new ArrayList<PageEntry>();
+
+        pageEntries.add(new PageEntry(PageType.TOP_SITES,
+                                      mContext.getString(R.string.home_top_sites_title),
+                                      EnumSet.of(PageEntry.Flags.DEFAULT_PAGE)));
+
+        pageEntries.add(new PageEntry(PageType.BOOKMARKS,
+                                      mContext.getString(R.string.bookmarks_title)));
+
+        // We disable reader mode support on low memory devices. Hence the
+        // reading list page should not show up on such devices.
+        if (!HardwareUtils.isLowMemoryPlatform()) {
+            pageEntries.add(new PageEntry(PageType.READING_LIST,
+                                          mContext.getString(R.string.reading_list_title)));
+        }
+
+        final PageEntry historyEntry = new PageEntry(PageType.HISTORY,
+                                                     mContext.getString(R.string.home_history_title));
+
+        // On tablets, the history page is the last.
+        // On phones, the history page is the first one.
+        if (HardwareUtils.isTablet()) {
+            pageEntries.add(historyEntry);
+        } else {
+            pageEntries.add(0, historyEntry);
+        }
+
+        return Collections.unmodifiableList(pageEntries);
+    }
+
+    public void save(List<PageEntry> entries) {
+        // This is a static backend, do nothing.
+    }
+
+    public void setOnChangeListener(OnChangeListener listener) {
+        // This is a static backend, do nothing.
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -4,48 +4,81 @@
  * 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.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.home.HomeAdapter.OnAddPageListener;
+import org.mozilla.gecko.home.HomeConfig.PageEntry;
+import org.mozilla.gecko.home.HomeConfig.PageType;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.HardwareUtils;
 
 import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
 import android.support.v4.view.ViewPager;
 import android.view.ViewGroup.LayoutParams;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.ViewGroup;
 import android.view.View;
 
 import java.util.EnumSet;
+import java.util.List;
 
 public class HomePager extends ViewPager {
 
+    private static final int LOADER_ID_CONFIG = 0;
+
     private final Context mContext;
     private volatile boolean mLoaded;
     private Decor mDecor;
+    private View mTabStrip;
 
     private final OnAddPageListener mAddPageListener;
 
+    private final HomeConfig mConfig;
+    private ConfigLoaderCallbacks mConfigLoaderCallbacks;
+
+    private Page mInitialPage;
+
     // List of pages in order.
     @RobocopTarget
     public enum Page {
         HISTORY,
         TOP_SITES,
         BOOKMARKS,
-        READING_LIST
+        READING_LIST;
+
+        static Page valueOf(PageType page) {
+            switch(page) {
+                case TOP_SITES:
+                    return Page.TOP_SITES;
+
+                case BOOKMARKS:
+                    return Page.BOOKMARKS;
+
+                case HISTORY:
+                    return Page.HISTORY;
+
+                case READING_LIST:
+                    return Page.READING_LIST;
+
+                default:
+                    throw new IllegalArgumentException("Could not convert unrecognized PageType");
+            }
+        }
     }
 
     // This is mostly used by UI tests to easily fetch
     // specific list views at runtime.
     static final String LIST_TAG_HISTORY = "history";
     static final String LIST_TAG_BOOKMARKS = "bookmarks";
     static final String LIST_TAG_READING_LIST = "reading_list";
     static final String LIST_TAG_TOP_SITES = "top_sites";
@@ -76,25 +109,29 @@ public class HomePager extends ViewPager
         public void onAddPagerView(String title);
         public void removeAllPagerViews();
         public void onPageSelected(int position);
         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
         public void setOnTitleClickListener(OnTitleClickListener onTitleClickListener);
     }
 
     static final String CAN_LOAD_ARG = "canLoad";
+    static final String PAGE_ENTRY_ARG = "pageEntry";
 
     public HomePager(Context context) {
         this(context, null);
     }
 
     public HomePager(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
 
+        mConfig = HomeConfig.getDefault(mContext);
+        mConfigLoaderCallbacks = new ConfigLoaderCallbacks();
+
         mAddPageListener = new OnAddPageListener() {
             @Override
             public void onAddPage(String title) {
                 if (mDecor != null) {
                     mDecor.onAddPagerView(title);
                 }
             }
         };
@@ -111,16 +148,17 @@ public class HomePager extends ViewPager
         setFocusableInTouchMode(true);
     }
 
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         if (child instanceof Decor) {
             ((ViewPager.LayoutParams) params).isDecor = true;
             mDecor = (Decor) child;
+            mTabStrip = child;
 
             mDecor.setOnTitleClickListener(new OnTitleClickListener() {
                 @Override
                 public void onTitleClicked(int index) {
                     setCurrentItem(index, true);
                 }
             });
 
@@ -133,71 +171,56 @@ public class HomePager extends ViewPager
                 @Override
                 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                     mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels);
                 }
 
                 @Override
                 public void onPageScrollStateChanged(int state) { }
             });
+        } else if (child instanceof HomePagerTabStrip) {
+            mTabStrip = child;
         }
 
         super.addView(child, index, params);
     }
 
-    public void redisplay(FragmentManager fm) {
+    public void redisplay(LoaderManager lm, FragmentManager fm) {
         final HomeAdapter adapter = (HomeAdapter) getAdapter();
-        final Page currentPage = adapter.getPageAtPosition(getCurrentItem());
 
-        show(fm, currentPage, null);
+        Page currentPage = adapter.getPageAtPosition(getCurrentItem());
+        show(lm, fm, currentPage, null);
     }
 
     /**
      * Loads and initializes the pager.
      *
      * @param fm FragmentManager for the adapter
      */
-    public void show(FragmentManager fm, Page page, PropertyAnimator animator) {
+    public void show(LoaderManager lm, FragmentManager fm, Page page, PropertyAnimator animator) {
         mLoaded = true;
-
-        if (mDecor != null) {
-            mDecor.removeAllPagerViews();
-        }
-
-        final HomeAdapter adapter = new HomeAdapter(mContext, fm);
-        adapter.setOnAddPageListener(mAddPageListener);
+        mInitialPage = page;
 
         // Only animate on post-HC devices, when a non-null animator is given
         final boolean shouldAnimate = (animator != null && Build.VERSION.SDK_INT >= 11);
 
-        adapter.addPage(Page.TOP_SITES, TopSitesPage.class, new Bundle(),
-                getContext().getString(R.string.home_top_sites_title));
-        adapter.addPage(Page.BOOKMARKS, BookmarksPage.class, new Bundle(),
-                getContext().getString(R.string.bookmarks_title));
-
-        // We disable reader mode support on low memory devices. Hence the
-        // reading list page should not show up on such devices.
-        if (!HardwareUtils.isLowMemoryPlatform()) {
-            adapter.addPage(Page.READING_LIST, ReadingListPage.class, new Bundle(),
-                    getContext().getString(R.string.reading_list_title));
-        }
-
-        // On phones, the history page is the first one. On tablets, the
-        // history page is the last.
-        adapter.addPage(HardwareUtils.isTablet() ? -1 : 0,
-                Page.HISTORY, HistoryPage.class, new Bundle(),
-                getContext().getString(R.string.home_history_title));
-
+        final HomeAdapter adapter = new HomeAdapter(mContext, fm);
+        adapter.setOnAddPageListener(mAddPageListener);
         adapter.setCanLoadHint(!shouldAnimate);
-
         setAdapter(adapter);
 
-        setCurrentItem(adapter.getItemPosition(page), false);
         setVisibility(VISIBLE);
 
+        // Don't show the tabs strip until we have the
+        // list of pages in place.
+        mTabStrip.setVisibility(View.INVISIBLE);
+
+        // Load list of pages from configuration
+        lm.initLoader(LOADER_ID_CONFIG, null, mConfigLoaderCallbacks);
+
         if (shouldAnimate) {
             animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
                 @Override
                 public void onPropertyAnimationStart() {
                     setLayerType(View.LAYER_TYPE_HARDWARE, null);
                 }
 
                 @Override
@@ -249,9 +272,71 @@ public class HomePager extends ViewPager
     public boolean onInterceptTouchEvent(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
             // Drop the soft keyboard by stealing focus from the URL bar.
             requestFocus();
         }
 
         return super.onInterceptTouchEvent(event);
     }
+
+    private void updateUiFromPageEntries(List<PageEntry> pageEntries) {
+        // 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();
+        }
+
+        final HomeAdapter adapter = (HomeAdapter) getAdapter();
+
+        // Disable loading until the final current item is defined
+        // after loading the page entries. This is to stop any temporary
+        // active item from loading.
+        boolean originalCanLoadHint = adapter.getCanLoadHint();
+        adapter.setCanLoadHint(false);
+
+        // Update the adapter with the new page entries
+        adapter.update(pageEntries);
+
+        final int count = (pageEntries != null ? pageEntries.size() : 0);
+        mTabStrip.setVisibility(count > 0 ? View.VISIBLE : View.INVISIBLE);
+
+        // Use the default page as defined in the HomePager's configuration
+        // if the initial page wasn't explicitly set by the show() caller.
+        if (mInitialPage != null) {
+            setCurrentItem(adapter.getItemPosition(mInitialPage), false);
+            mInitialPage = null;
+        } else {
+            for (int i = 0; i < count; i++) {
+                final PageEntry pageEntry = pageEntries.get(i);
+                if (pageEntry.isDefault()) {
+                    setCurrentItem(i, false);
+                    break;
+                }
+            }
+        }
+
+        // Restore canLoadHint now that we have the final
+        // state in HomePager.
+        adapter.setCanLoadHint(originalCanLoadHint);
+    }
+
+    private class ConfigLoaderCallbacks implements LoaderCallbacks<List<PageEntry>> {
+        @Override
+        public Loader<List<PageEntry>> onCreateLoader(int id, Bundle args) {
+            return new HomeConfigLoader(mContext, mConfig);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<List<PageEntry>> loader, List<PageEntry> pageEntries) {
+            updateUiFromPageEntries(pageEntries);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<List<PageEntry>> loader) {
+            updateUiFromPageEntries(null);
+        }
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/ListPage.java
@@ -0,0 +1,185 @@
+/* -*- 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.home.HomePager.OnUrlOpenListener;
+import org.mozilla.gecko.home.HomeConfig.PageEntry;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.CursorAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import java.util.EnumSet;
+
+/**
+ * Fragment that displays custom lists.
+ */
+public class ListPage extends HomeFragment {
+    // Cursor loader ID for the lists
+    private static final int LOADER_ID_LIST = 0;
+
+    // The page entry associated with this page
+    private PageEntry mPageEntry;
+
+    // Adapter for the list
+    private HomeListAdapter mAdapter;
+
+    // The view shown by the fragment
+    private ListView mList;
+
+    // Callbacks used for the list loader
+    private CursorLoaderCallbacks mCursorLoaderCallbacks;
+
+    // On URL open listener
+    private OnUrlOpenListener mUrlOpenListener;
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        try {
+            mUrlOpenListener = (OnUrlOpenListener) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString()
+                    + " must implement HomePager.OnUrlOpenListener");
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+
+        mUrlOpenListener = null;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final Bundle args = getArguments();
+        if (args != null) {
+            mPageEntry = (PageEntry) args.getParcelable(HomePager.PAGE_ENTRY_ARG);
+        }
+
+        if (mPageEntry == null) {
+            throw new IllegalStateException("Can't create a ListPage without a PageEntry");
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        mList = new HomeListView(getActivity());
+        return mList;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        registerForContextMenu(mList);
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        mList = null;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        // Detach and reattach the fragment as the layout changes.
+        if (isVisible()) {
+            getFragmentManager().beginTransaction()
+                                .detach(this)
+                                .attach(this)
+                                .commitAllowingStateLoss();
+        }
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        mAdapter = new HomeListAdapter(getActivity(), null);
+        mList.setAdapter(mAdapter);
+
+        // Create callbacks before the initial loader is started.
+        mCursorLoaderCallbacks = new CursorLoaderCallbacks();
+        loadIfVisible();
+    }
+
+    @Override
+    protected void load() {
+        getLoaderManager().initLoader(LOADER_ID_LIST, null, mCursorLoaderCallbacks);
+    }
+
+    /**
+     * Cursor loader for the lists.
+     */
+    private static class HomeListLoader extends SimpleCursorLoader {
+        public HomeListLoader(Context context) {
+            super(context);
+        }
+
+        @Override
+        public Cursor loadCursor() {
+            // Do nothing
+            return null;
+        }
+    }
+
+    /**
+     * Cursor adapter for the list.
+     */
+    private class HomeListAdapter extends CursorAdapter {
+        public HomeListAdapter(Context context, Cursor cursor) {
+            super(context, cursor, 0);
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            final TwoLinePageRow row = (TwoLinePageRow) view;
+            row.updateFromCursor(cursor);
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            return LayoutInflater.from(parent.getContext()).inflate(R.layout.bookmark_item_row, parent, false);
+        }
+    }
+
+    /**
+     * LoaderCallbacks implementation that interacts with the LoaderManager.
+     */
+    private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
+        @Override
+        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            return new HomeListLoader(getActivity());
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
+            mAdapter.swapCursor(c);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {
+            mAdapter.swapCursor(null);
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -205,21 +205,25 @@ gbjar.sources += [
     'home/BookmarksListAdapter.java',
     'home/BookmarksListView.java',
     'home/BookmarksPage.java',
     'home/BrowserSearch.java',
     'home/FadedTextView.java',
     'home/HistoryPage.java',
     'home/HomeAdapter.java',
     'home/HomeBanner.java',
+    'home/HomeConfig.java',
+    'home/HomeConfigLoader.java',
+    'home/HomeConfigMemBackend.java',
     'home/HomeFragment.java',
     'home/HomeListView.java',
     'home/HomePager.java',
     'home/HomePagerTabStrip.java',
     'home/LastTabsPage.java',
+    'home/ListPage.java',
     'home/MostRecentPage.java',
     'home/MultiTypeCursorAdapter.java',
     'home/PinSiteDialog.java',
     'home/ReadingListPage.java',
     'home/SearchEngine.java',
     'home/SearchEngineRow.java',
     'home/SearchLoader.java',
     'home/SimpleCursorLoader.java',