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 170398 fe13b62902c8070f4bd59bf0b42f7bd9936c2eaf
parent 170397 ce3dd14c1840cd81593dd4eba22e4659719b4769
child 170399 adb909c5d6c3edcd0c5482815a4558f0839a4184
push idunknown
push userunknown
push dateunknown
reviewersmargaret
bugs942231
milestone29.0a1
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',