Bug 868553 - Implement HomePager for about:home views. r=lucasr
authorBrian Nicholson <bnicholson@mozilla.com>
Mon, 13 May 2013 14:06:34 -0700
changeset 151280 f8d0784186b7bf7a09a5729bdf4f450bd5e791bd
parent 138923 81dd97739fa11433d1dd904be92dfcb5e6432af6
child 151281 9ab2cb00222facafdfa53dce2029dc25ad5f5e7a
push idunknown
push userunknown
push dateunknown
reviewerslucasr
bugs868553
milestone24.0a1
Bug 868553 - Implement HomePager for about:home views. r=lucasr
mobile/android/base/BrowserApp.java
mobile/android/base/Makefile.in
mobile/android/base/home/HomePager.java
mobile/android/base/resources/layout/abouthome_content.xml
mobile/android/base/resources/layout/gecko_app.xml
mobile/android/base/widget/AboutHome.java
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -8,16 +8,17 @@ package org.mozilla.gecko;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.GeckoLayerClient;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PanZoomController;
+import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UiAsyncTask;
 import org.mozilla.gecko.widget.AboutHome;
 
 import org.json.JSONArray;
@@ -77,17 +78,17 @@ abstract public class BrowserApp extends
 
     private static final int TABS_ANIMATION_DURATION = 450;
 
     private static final int READER_ADD_SUCCESS = 0;
     private static final int READER_ADD_FAILED = 1;
     private static final int READER_ADD_DUPLICATE = 2;
 
     public static BrowserToolbar mBrowserToolbar;
-    private AboutHome mAboutHome;
+    private HomePager mHomePager;
     protected Telemetry.Timer mAboutHomeStartupTimer = null;
 
     private static final int ADDON_MENU_OFFSET = 1000;
     private class MenuItemInfo {
         public int id;
         public String label;
         public String icon;
         public boolean checkable;
@@ -119,43 +120,39 @@ abstract public class BrowserApp extends
     // Whether the dynamic toolbar pref is enabled.
     private boolean mDynamicToolbarEnabled = false;
 
     // Stored value of the toolbar height, so we know when it's changed.
     private int mToolbarHeight = 0;
 
     private Integer mPrefObserverId;
 
-    // Tag for the AboutHome fragment. The fragment is automatically attached
-    // after restoring from a saved state, so we use this tag to identify it.
-    private static final String ABOUTHOME_TAG = "abouthome";
-
     private SharedPreferencesHelper mSharedPreferencesHelper;
 
     private OrderedBroadcastHelper mOrderedBroadcastHelper;
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         switch(msg) {
             case LOCATION_CHANGE:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     maybeCancelFaviconLoad(tab);
                 }
                 // fall through
             case SELECTED:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     if ("about:home".equals(tab.getURL())) {
-                        showAboutHome();
+                        showHomePager();
 
                         if (isDynamicToolbarEnabled()) {
                             // Show the toolbar.
                             mLayerView.getLayerMarginsAnimator().showMargins(false);
                         }
                     } else {
-                        hideAboutHome();
+                        hideHomePager();
                     }
 
                     // Dismiss any SiteIdentity Popup
                     SiteIdentityPopup.getInstance().dismiss();
 
                     final TabsPanel.Panel panel = tab.isPrivate()
                                                 ? TabsPanel.Panel.PRIVATE_TABS
                                                 : TabsPanel.Panel.NORMAL_TABS;
@@ -219,17 +216,17 @@ abstract public class BrowserApp extends
 
         // Gamepad support only exists in API-level >= 9
         if (Build.VERSION.SDK_INT >= 9 &&
             (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
             switch (keyCode) {
                 case KeyEvent.KEYCODE_BUTTON_Y:
                     // Toggle/focus the address bar on gamepad-y button.
                     if (mBrowserToolbar.isVisible()) {
-                        if (isDynamicToolbarEnabled() && !mAboutHome.getUserVisibleHint()) {
+                        if (isDynamicToolbarEnabled() && !mHomePager.isVisible()) {
                             if (mLayerView != null) {
                                 mLayerView.getLayerMarginsAnimator().hideMargins(false);
                                 mLayerView.requestFocus();
                             }
                         } else {
                             // Just focus the address bar when about:home is visible
                             // or when the dynamic toolbar isn't enabled.
                             mBrowserToolbar.requestFocusFromTouch();
@@ -328,18 +325,17 @@ abstract public class BrowserApp extends
         });
     }
 
     @Override
     void onStatePurged() {
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
-                if (mAboutHome != null)
-                    mAboutHome.setLastTabsVisibility(false);
+                mHomePager.setAboutHomeLastTabsVisibility(false);
             }
         });
 
         super.onStatePurged();
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -352,38 +348,27 @@ abstract public class BrowserApp extends
 
         ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideTabsTouchListener());
         ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() {
             @Override
             public boolean onInterceptMotionEvent(View view, MotionEvent event) {
                 // If we get a gamepad panning MotionEvent while the focus is not on the layerview,
                 // put the focus on the layerview and carry on
                 if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) {
-                    if (mAboutHome.getUserVisibleHint()) {
+                    if (mHomePager.isVisible()) {
                         mLayerView.requestFocus();
                     } else {
-                        mAboutHome.requestFocus();
+                        mHomePager.requestFocus();
                     }
                 }
                 return false;
             }
         });
 
-        // Find the Fragment if it was already added from a restored instance state.
-        mAboutHome = (AboutHome) getSupportFragmentManager().findFragmentByTag(ABOUTHOME_TAG);
-
-        if (mAboutHome == null) {
-            // AboutHome will be dynamically attached and detached as
-            // about:home is shown. Adding/removing the fragment is not synchronous,
-            // so we can't use Fragment#isVisible() to determine whether the
-            // about:home is shown. Instead, we use Fragment#getUserVisibleHint()
-            // with the hint we set ourselves.
-            mAboutHome = AboutHome.newInstance();
-            mAboutHome.setUserVisibleHint(false);
-        }
+        mHomePager = (HomePager) findViewById(R.id.abouthome_pager);
 
         mBrowserToolbar = new BrowserToolbar(this);
         mBrowserToolbar.from(actionBar);
 
         // Intercept key events for gamepad shortcuts
         actionBar.setOnKeyListener(this);
 
         if (mTabsPanel != null) {
@@ -458,17 +443,17 @@ abstract public class BrowserApp extends
             }
             setToolbarMargin(0);
         } else {
             // Immediately show the toolbar when disabling the dynamic
             // toolbar.
             if (mLayerView != null) {
                 mLayerView.getLayerClient().setOnMetricsChangedListener(null);
             }
-            mAboutHome.setPadding(0, 0, 0, 0);
+            mHomePager.setPadding(0, 0, 0, 0);
             if (mBrowserToolbar != null) {
                 mBrowserToolbar.getLayout().scrollTo(0, 0);
             }
         }
 
         refreshToolbarHeight();
     }
 
@@ -575,32 +560,32 @@ abstract public class BrowserApp extends
 
     private void setToolbarMargin(int margin) {
         ((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin;
         mGeckoLayout.requestLayout();
     }
 
     @Override
     public void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
-        if (mAboutHome.getUserVisibleHint() || mBrowserToolbar == null) {
+        if (mHomePager.isVisible() || mBrowserToolbar == null) {
             return;
         }
 
         final View toolbarLayout = mBrowserToolbar.getLayout();
         final int marginTop = Math.round(aMetrics.marginTop);
         ThreadUtils.postToUiThread(new Runnable() {
             public void run() {
                 toolbarLayout.scrollTo(0, toolbarLayout.getHeight() - marginTop);
             }
         });
     }
 
     @Override
     public void onPanZoomStopped() {
-        if (!isDynamicToolbarEnabled() || mAboutHome.getUserVisibleHint()) {
+        if (!isDynamicToolbarEnabled() || mHomePager.isVisible()) {
             return;
         }
 
         ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
         if (metrics.marginTop >= mToolbarHeight / 2) {
             mLayerView.getLayerMarginsAnimator().showMargins(false);
         } else {
             mLayerView.getLayerMarginsAnimator().hideMargins(false);
@@ -608,25 +593,25 @@ abstract public class BrowserApp extends
     }
 
     public void refreshToolbarHeight() {
         int height = 0;
         if (mBrowserToolbar != null) {
             height = mBrowserToolbar.getLayout().getHeight();
         }
 
-        if (!isDynamicToolbarEnabled() || mAboutHome.getUserVisibleHint()) {
+        if (!isDynamicToolbarEnabled() || mHomePager.isVisible()) {
             // Use aVisibleHeight here so that when the dynamic toolbar is
             // enabled, the padding will animate with the toolbar becoming
             // visible.
             if (isDynamicToolbarEnabled()) {
                 // When the dynamic toolbar is enabled, set the padding on the
                 // about:home widget directly - this is to avoid resizing the
                 // LayerView, which can cause visible artifacts.
-                mAboutHome.setPadding(0, height, 0, 0);
+                mHomePager.setPadding(0, height, 0, 0);
             } else {
                 setToolbarMargin(height);
                 height = 0;
             }
         } else {
             setToolbarMargin(0);
         }
 
@@ -1023,56 +1008,44 @@ abstract public class BrowserApp extends
 
         // Reset favicon load state
         tab.setFaviconLoadId(Favicons.NOT_LOADING);
     }
 
 
     /* About:home UI */
     void updateAboutHomeTopSites() {
-        mAboutHome.update(EnumSet.of(AboutHome.UpdateFlags.TOP_SITES));
+        mHomePager.updateAboutHome(EnumSet.of(AboutHome.UpdateFlags.TOP_SITES));
     }
 
-    private void showAboutHome() {
-        if (mAboutHome.getUserVisibleHint()) {
+    private void showHomePager() {
+        if (mHomePager.isVisible()) {
             return;
         }
 
         // Refresh toolbar height to possibly restore the toolbar padding
         refreshToolbarHeight();
 
         // Show the toolbar before hiding about:home so the
         // onMetricsChanged callback still works.
         if (isDynamicToolbarEnabled() && mLayerView != null) {
             mLayerView.getLayerMarginsAnimator().showMargins(true);
         }
 
-        // We use commitAllowingStateLoss() instead of commit() here to avoid an
-        // IllegalStateException. showAboutHome() and hideAboutHome() are
-        // executed inside of tab's onChange() callback. Since that callback can
-        // be triggered asynchronously from Gecko, it's possible that this
-        // method can be called while Fennec is in the background. If that
-        // happens, using commit() would throw an IllegalStateException since
-        // it can't be used between the Activity's onSaveInstanceState() and
-        // onResume().
-        getSupportFragmentManager().beginTransaction()
-                .add(R.id.gecko_layout, mAboutHome, ABOUTHOME_TAG).commitAllowingStateLoss();
-        mAboutHome.setUserVisibleHint(true);
+        mHomePager.show(getSupportFragmentManager());
 
         mBrowserToolbar.setNextFocusDownId(R.id.abouthome_content);
     }
 
-    private void hideAboutHome() {
-        if (!mAboutHome.getUserVisibleHint()) {
+    private void hideHomePager() {
+        if (!mHomePager.isVisible()) {
             return;
         }
 
-        getSupportFragmentManager().beginTransaction()
-                .remove(mAboutHome).commitAllowingStateLoss();
-        mAboutHome.setUserVisibleHint(false);
+        mHomePager.hide();
 
         mBrowserToolbar.setShadowVisibility(true);
         mBrowserToolbar.setNextFocusDownId(R.id.layer_view);
 
         // Refresh toolbar height to possibly restore the toolbar padding
         refreshToolbarHeight();
     }
 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -215,16 +215,17 @@ FENNEC_JAVA_FILES = \
   gfx/SubdocumentScrollHelper.java \
   gfx/TextLayer.java \
   gfx/TextureGenerator.java \
   gfx/TextureReaper.java \
   gfx/TileLayer.java \
   gfx/TouchEventHandler.java \
   gfx/ViewTransform.java \
   gfx/VirtualLayer.java \
+  home/HomePager.java \
   widget/AboutHome.java \
   widget/AboutHomeView.java \
   widget/AboutHomeSection.java \
   widget/AddonsSection.java \
   widget/DateTimePicker.java \
   widget/FaviconView.java \
   widget/IconTabWidget.java \
   widget/LastTabsSection.java \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/HomePager.java
@@ -0,0 +1,149 @@
+/* -*- 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 java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.EnumSet;
+
+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.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+
+import org.mozilla.gecko.widget.AboutHome;
+
+public class HomePager extends ViewPager {
+    private final Context mContext;
+    private volatile boolean mLoaded;
+
+    private enum Page {
+        ABOUT_HOME
+    }
+
+    private EnumMap<Page, Fragment> mPages = new EnumMap<Page, Fragment>(Page.class);
+
+    public HomePager(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    public HomePager(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    /**
+     * Loads and initializes the pager.
+     *
+     * @param fm FragmentManager for the adapter
+     */
+    public void show(FragmentManager fm) {
+        mLoaded = true;
+        TabsAdapter adapter = new TabsAdapter(fm);
+        adapter.addTab(Page.ABOUT_HOME, AboutHome.class, null);
+        setAdapter(adapter);
+        setVisibility(VISIBLE);
+    }
+
+    /**
+     * Hides the pager and removes all child fragments.
+     */
+    public void hide() {
+        mLoaded = false;
+        setVisibility(GONE);
+        setAdapter(null);
+    }
+
+    /**
+     * Determines whether the pager is visible.
+     *
+     * Unlike getVisibility(), this method does not need to be called on the UI
+     * thread.
+     *
+     * @return Whether the pager and its fragments are being displayed
+     */
+    public boolean isVisible() {
+        return mLoaded;
+    }
+
+    /**
+     * @see AboutHome#update(EnumSet)
+     */
+    public void updateAboutHome(final EnumSet<AboutHome.UpdateFlags> flags) {
+        AboutHome aboutHome = (AboutHome) mPages.get(Page.ABOUT_HOME);
+        if (aboutHome != null) {
+            aboutHome.update(flags);
+        }
+    }
+
+    /**
+     * @see AboutHome#setLastTabsVisibility(boolean)
+     */
+    public void setAboutHomeLastTabsVisibility(boolean visible) {
+        AboutHome aboutHome = (AboutHome) mPages.get(Page.ABOUT_HOME);
+        if (aboutHome != null) {
+            aboutHome.setLastTabsVisibility(visible);
+        }
+    }
+
+    class TabsAdapter extends FragmentStatePagerAdapter {
+        private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
+
+        final class TabInfo {
+            private final Page page;
+            private final Class<?> clss;
+            private final Bundle args;
+
+            TabInfo(Page page, Class<?> clss, Bundle args) {
+                this.page = page;
+                this.clss = clss;
+                this.args = args;
+            }
+        }
+
+        public TabsAdapter(FragmentManager fm) {
+            super(fm);
+        }
+
+        public void addTab(Page page, Class<?> clss, Bundle args) {
+            TabInfo info = new TabInfo(page, clss, args);
+            mTabs.add(info);
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public int getCount() {
+            return mTabs.size();
+        }
+
+        @Override
+        public Fragment getItem(int position) {
+            TabInfo info = mTabs.get(position);
+            return Fragment.instantiate(mContext, info.clss.getName(), info.args);
+        }
+
+        @Override
+        public Object instantiateItem(ViewGroup container, int position) {
+            Fragment fragment = (Fragment) super.instantiateItem(container, position);
+
+            mPages.put(mTabs.get(position).page, fragment);
+
+            return fragment;
+        }
+
+        @Override
+        public void destroyItem(ViewGroup container, int position, Object object) {
+            super.destroyItem(container, position, object);
+
+            mPages.remove(mTabs.get(position).page);
+        }
+    }
+}
--- a/mobile/android/base/resources/layout/abouthome_content.xml
+++ b/mobile/android/base/resources/layout/abouthome_content.xml
@@ -1,20 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <org.mozilla.gecko.widget.AboutHomeView
-		xmlns:android="http://schemas.android.com/apk/res/android"
-		xmlns:gecko="http://schemas.android.com/apk/res-auto"
-		android:id="@+id/abouthome_content"
-		android:layout_width="fill_parent"
-		android:layout_height="fill_parent"
-		android:background="@color/background_normal">
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:gecko="http://schemas.android.com/apk/res-auto"
+        android:id="@+id/abouthome_content"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent">
 
     <RelativeLayout android:layout_width="fill_parent"
                     android:layout_height="fill_parent"
                     android:paddingLeft="@dimen/abouthome_gutter_large"
                     android:paddingRight="@dimen/abouthome_gutter_large">
 
         <Gecko.ImageView android:id="@+id/abouthome_logo"
                          android:src="@drawable/abouthome_logo"
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -23,16 +23,22 @@
 
         <RelativeLayout android:id="@+id/gecko_layout"
                         android:layout_width="fill_parent"
                         android:layout_height="fill_parent"
                         android:layout_above="@+id/find_in_page">
 
             <include layout="@layout/shared_ui_components"/>
 
+            <org.mozilla.gecko.home.HomePager android:id="@+id/abouthome_pager"
+                                              android:layout_width="fill_parent"
+                                              android:layout_height="fill_parent"
+                                              android:background="@color/background_normal"
+                                              android:visibility="gone"/>
+
         </RelativeLayout>
 
         <org.mozilla.gecko.FindInPageBar android:id="@+id/find_in_page"
                                          android:layout_width="fill_parent"
                                          android:layout_height="wrap_content"
                                          android:layout_alignParentBottom="true"
                                          style="@style/FindBar"
                                          android:visibility="gone"/>
--- a/mobile/android/base/widget/AboutHome.java
+++ b/mobile/android/base/widget/AboutHome.java
@@ -32,38 +32,30 @@ public class AboutHome extends Fragment 
 
         public static final EnumSet<UpdateFlags> ALL = EnumSet.allOf(UpdateFlags.class);
     }
 
     private UriLoadListener mUriLoadListener;
     private LoadCompleteListener mLoadCompleteListener;
     private LightweightTheme mLightweightTheme;
     private ContentObserver mTabsContentObserver;
-    private int mPaddingLeft;
-    private int mPaddingRight;
-    private int mPaddingTop;
-    private int mPaddingBottom;
     private AboutHomeView mAboutHomeView;
     private AddonsSection mAddonsSection;
     private LastTabsSection mLastTabsSection;
     private RemoteTabsSection mRemoteTabsSection;
     private TopSitesView mTopSitesView;
 
     public interface UriLoadListener {
         public void onAboutHomeUriLoad(String uriSpec);
     }
 
     public interface LoadCompleteListener {
         public void onAboutHomeLoadComplete();
     }
 
-    public static AboutHome newInstance() {
-        return new AboutHome();
-    }
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
         mLightweightTheme = ((GeckoApplication) getActivity().getApplication()).getLightweightTheme();
     }
 
     @Override
@@ -108,17 +100,16 @@ public class AboutHome extends Fragment 
 
         return mAboutHomeView;
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
 
-        view.setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom);
         ((PromoBox) view.findViewById(R.id.promo_box)).showRandomPromo();
         update(AboutHome.UpdateFlags.ALL);
 
         mTopSitesView.setLoadCompleteListener(mLoadCompleteListener);
         mTopSitesView.setUriLoadListener(mUriLoadListener);
         mAddonsSection.setUriLoadListener(mUriLoadListener);
 
         // Reload the mobile homepage on inbound tab syncs
@@ -229,31 +220,9 @@ public class AboutHome extends Fragment 
             return;
         }
 
         if (visible)
             mLastTabsSection.show();
         else
             mLastTabsSection.hide();
     }
-
-    public void requestFocus() {
-        View view = getView();
-        if (view != null) {
-            view.requestFocus();
-        }
-    }
-
-    public void setPadding(int left, int top, int right, int bottom) {
-        View view = getView();
-        if (view != null) {
-            view.setPadding(left, top, right, bottom);
-        }
-
-        // If the padding has changed but the view hasn't been created yet,
-        // store the padding values here; they will be used later in
-        // onViewCreated().
-        mPaddingLeft = left;
-        mPaddingRight = right;
-        mPaddingTop = top;
-        mPaddingBottom = bottom;
-    }
 }