Bug 1060544 - Part 1: allow persisting/restoring home panel state r?liuche draft
authorAndrzej Hunt <ahunt@mozilla.com>
Fri, 29 Apr 2016 16:00:40 +0200
changeset 357839 b92caa5385fbcc98ea43691253e9c70a9396b762
parent 357833 741a8fcef3314464ae380646d52db9f42943fc32
child 357840 c462287c36786d7644b1523db1393b891945a9cf
push id16858
push userahunt@mozilla.com
push dateFri, 29 Apr 2016 14:52:18 +0000
reviewersliuche
bugs1060544
milestone49.0a1
Bug 1060544 - Part 1: allow persisting/restoring home panel state r?liuche All home panel state is currently discarded when you navigate to another page. We want to be able to restore state (e.g. to return to the currently opened bookmarks folder, instead of returning to the root of the bookmarks folder). This commit adds storage and a framework to allow homepanels to persist data. MozReview-Commit-ID: GWDX7UZrIZs
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/Tab.java
mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
mobile/android/base/java/org/mozilla/gecko/home/HomePager.java
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -38,16 +38,17 @@ import org.mozilla.gecko.gfx.DynamicTool
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.home.BrowserSearch;
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.home.HomeConfigPrefsBackend;
+import org.mozilla.gecko.home.HomeFragment;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.javaaddons.JavaAddonManager;
 import org.mozilla.gecko.media.AudioFocusAgent;
 import org.mozilla.gecko.menu.GeckoMenu;
@@ -2427,17 +2428,17 @@ public class BrowserApp extends GeckoApp
 
         mBrowserToolbar.startEditing(url, animator);
 
         final boolean isUserSearchTerm = selectedTab != null &&
                 !TextUtils.isEmpty(selectedTab.getUserRequested());
         if (isUserSearchTerm && SwitchBoard.isInExperiment(getContext(), Experiments.SEARCH_TERM)) {
             showBrowserSearchAfterAnimation(animator);
         } else {
-            showHomePagerWithAnimator(panelId, animator);
+            showHomePagerWithAnimator(panelId, null, animator);
         }
 
         animator.start();
         Telemetry.startUISession(TelemetryContract.Session.AWESOMESCREEN);
     }
 
     private void commitEditingMode() {
         if (!mBrowserToolbar.isEditing()) {
@@ -2618,24 +2619,30 @@ public class BrowserApp extends GeckoApp
      * Shows or hides the home pager for the given tab.
      */
     private void updateHomePagerForTab(Tab tab) {
         // Don't change the visibility of the home pager if we're in editing mode.
         if (mBrowserToolbar.isEditing()) {
             return;
         }
 
+        // History will only store that we were visiting about:home, however the specific panel
+        // isn't stored. (We are able to navigate directly to homepanels using an about:home?panel=...
+        // URL, but the reverse doesn't apply: manually switching panels doesn't update the URL.)
+        // Hence we need to restore the panel, in addition to panel state, here.
         if (isAboutHome(tab)) {
             String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL());
+            Bundle panelRestoreData = null;
             if (panelId == null) {
                 // No panel was specified in the URL. Try loading the most recent
                 // home panel for this tab.
                 panelId = tab.getMostRecentHomePanel();
+                panelRestoreData = tab.getMostRecentHomePanelData();
             }
-            showHomePager(panelId);
+            showHomePager(panelId, panelRestoreData);
 
             if (mDynamicToolbar.isEnabled()) {
                 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
             }
         } else {
             hideHomePager();
         }
     }
@@ -2714,24 +2721,24 @@ public class BrowserApp extends GeckoApp
                     }
                 }
             });
         }
 
         mHomePagerContainer.setVisibility(View.VISIBLE);
     }
 
-    private void showHomePager(String panelId) {
-        showHomePagerWithAnimator(panelId, null);
+    private void showHomePager(String panelId, Bundle panelRestoreData) {
+        showHomePagerWithAnimator(panelId, panelRestoreData, null);
     }
 
-    private void showHomePagerWithAnimator(String panelId, PropertyAnimator animator) {
+    private void showHomePagerWithAnimator(String panelId, Bundle panelRestoreData, PropertyAnimator animator) {
         if (isHomePagerVisible()) {
             // Home pager already visible, make sure it shows the correct panel.
-            mHomePager.showPanel(panelId);
+            mHomePager.showPanel(panelId, panelRestoreData);
             return;
         }
 
         // This must be called before the dynamic toolbar is set visible because it calls
         // FormAssistPopup.onMetricsChanged, which queues a runnable that undoes the effect of hide.
         // With hide first, onMetricsChanged will return early instead.
         mFormAssistPopup.hide();
         mFindInPageBar.hide();
@@ -2754,16 +2761,26 @@ public class BrowserApp extends GeckoApp
                 public void onPanelSelected(String panelId) {
                     final Tab currentTab = Tabs.getInstance().getSelectedTab();
                     if (currentTab != null) {
                         currentTab.setMostRecentHomePanel(panelId);
                     }
                 }
             });
 
+            mHomePager.setPanelStateChangelistener(new HomeFragment.PanelStateChangeListener() {
+                @Override
+                public void onStateChanged(Bundle bundle) {
+                    final Tab currentTab = Tabs.getInstance().getSelectedTab();
+                    if (currentTab != null) {
+                        currentTab.setMostRecentHomePanelData(bundle);
+                    }
+                }
+            });
+
             // Don't show the banner in guest mode.
             if (!Restrictions.isUserRestricted()) {
                 final ViewStub homeBannerStub = (ViewStub) findViewById(R.id.home_banner_stub);
                 final HomeBanner homeBanner = (HomeBanner) homeBannerStub.inflate();
                 mHomePager.setBanner(homeBanner);
 
                 // Remove the banner from the view hierarchy if it is dismissed.
                 homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() {
@@ -2774,17 +2791,19 @@ public class BrowserApp extends GeckoApp
                     }
                 });
             }
         }
 
         mHomePagerContainer.setVisibility(View.VISIBLE);
         mHomePager.load(getSupportLoaderManager(),
                         getSupportFragmentManager(),
-                        panelId, animator);
+                        panelId,
+                        panelRestoreData,
+                        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();
@@ -2930,17 +2949,18 @@ public class BrowserApp extends GeckoApp
 
     private void hideBrowserSearch() {
         if (!mBrowserSearch.getUserVisibleHint()) {
             return;
         }
 
         // To prevent overdraw, the HomePager is hidden when BrowserSearch is displayed:
         // reverse that.
-        showHomePager(Tabs.getInstance().getSelectedTab().getMostRecentHomePanel());
+        showHomePager(Tabs.getInstance().getSelectedTab().getMostRecentHomePanel(),
+                Tabs.getInstance().getSelectedTab().getMostRecentHomePanelData());
 
         mBrowserSearchContainer.setVisibility(View.INVISIBLE);
 
         getSupportFragmentManager().beginTransaction()
                 .hide(mBrowserSearch).commitAllowingStateLoss();
         mBrowserSearch.setUserVisibleHint(false);
 
         getWindow().setBackgroundDrawable(null);
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -31,16 +31,17 @@ import org.mozilla.gecko.toolbar.Browser
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.os.Build;
+import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 import org.mozilla.gecko.widget.SiteLogins;
 
 public class Tab {
     private static final String LOGTAG = "GeckoTab";
 
@@ -78,17 +79,24 @@ public class Tab {
     private Bitmap mThumbnailBitmap;
     private boolean mDesktopMode;
     private boolean mEnteringReaderMode;
     private final Context mAppContext;
     private ErrorType mErrorType = ErrorType.NONE;
     private volatile int mLoadProgress;
     private volatile int mRecordingCount;
     private volatile boolean mIsAudioPlaying;
+
     private String mMostRecentHomePanel;
+    /*
+     * Bundle containing restore data for the panel referenced in mMostRecentHomePanel. This can be
+     * e.g. the most recent folder for the bookmarks panel, or any other state that should be
+     * persisted. This is then used e.g. when returning to homepanels via history.
+     */
+    private Bundle mMostRecentHomePanelData;
 
     private int mHistoryIndex;
     private int mHistorySize;
     private boolean mCanDoBack;
     private boolean mCanDoForward;
 
     private boolean mIsEditing;
     private final TabEditingState mEditingState = new TabEditingState();
@@ -211,18 +219,27 @@ public class Tab {
     public BitmapDrawable getThumbnail() {
         return mThumbnail;
     }
 
     public String getMostRecentHomePanel() {
         return mMostRecentHomePanel;
     }
 
+    public Bundle getMostRecentHomePanelData() {
+        return mMostRecentHomePanelData;
+    }
+
     public void setMostRecentHomePanel(String panelId) {
         mMostRecentHomePanel = panelId;
+        mMostRecentHomePanelData = null;
+    }
+
+    public void setMostRecentHomePanelData(Bundle data) {
+        mMostRecentHomePanelData = data;
     }
 
     public Bitmap getThumbnailBitmap(int width, int height) {
         if (mThumbnailBitmap != null) {
             // Bug 787318 - Honeycomb has a bug with bitmap caching, we can't
             // reuse the bitmap there.
             boolean honeycomb = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
                               && Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2);
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
@@ -18,34 +18,46 @@ import android.view.ViewGroup;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 
 public class HomeAdapter extends FragmentStatePagerAdapter {
 
     private final Context mContext;
     private final ArrayList<PanelInfo> mPanelInfos;
-    private final HashMap<String, Fragment> mPanels;
+    private final HashMap<String, HomeFragment> mPanels;
+    private final HashMap<String, Bundle> mRestoreBundles;
 
     private boolean mCanLoadHint;
 
     private OnAddPanelListener mAddPanelListener;
 
+    private HomeFragment.PanelStateChangeListener mPanelStateChangeListener = null;
+
     public interface OnAddPanelListener {
         void onAddPanel(String title);
     }
 
+    public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
+        mPanelStateChangeListener = listener;
+
+        for (Fragment fragment : mPanels.values()) {
+            ((HomeFragment) fragment).setPanelStateChangeListener(listener);
+        }
+    }
+
     public HomeAdapter(Context context, FragmentManager fm) {
         super(fm);
 
         mContext = context;
         mCanLoadHint = HomeFragment.DEFAULT_CAN_LOAD_HINT;
 
-        mPanelInfos = new ArrayList<PanelInfo>();
-        mPanels = new HashMap<String, Fragment>();
+        mPanelInfos = new ArrayList<>();
+        mPanels = new HashMap<>();
+        mRestoreBundles = new HashMap<>();
     }
 
     @Override
     public int getCount() {
         return mPanelInfos.size();
     }
 
     @Override
@@ -61,26 +73,50 @@ public class HomeAdapter extends Fragmen
             return info.getTitle().toUpperCase();
         }
 
         return null;
     }
 
     @Override
     public Object instantiateItem(ViewGroup container, int position) {
-        Fragment fragment = (Fragment) super.instantiateItem(container, position);
-        mPanels.put(mPanelInfos.get(position).getId(), fragment);
+        final HomeFragment fragment = (HomeFragment) super.instantiateItem(container, position);
+        fragment.setPanelStateChangeListener(mPanelStateChangeListener);
+
+        final String id = mPanelInfos.get(position).getId();
+        mPanels.put(id, fragment);
+
+        if (mRestoreBundles.containsKey(id)) {
+            fragment.restoreData(mRestoreBundles.get(id));
+            mRestoreBundles.remove(id);
+        }
 
         return fragment;
     }
 
+    public void setRestoreData(int position, Bundle data) {
+        final String id = mPanelInfos.get(position).getId();
+        final HomeFragment fragment = mPanels.get(id);
+
+        // We have no guarantees as to whether our desired fragment is instantiated yet: therefore
+        // we might need to either pass data to the fragment, or store it for later.
+        if (fragment != null) {
+            fragment.restoreData(data);
+        } else {
+            mRestoreBundles.put(id, data);
+        }
+
+    }
+
     @Override
     public void destroyItem(ViewGroup container, int position, Object object) {
+        final String id = mPanelInfos.get(position).getId();
+
         super.destroyItem(container, position, object);
-        mPanels.remove(mPanelInfos.get(position).getId());
+        mPanels.remove(id);
     }
 
     public void setOnAddPanelListener(OnAddPanelListener listener) {
         mAddPanelListener = listener;
     }
 
     public int getItemPosition(String panelId) {
         for (int i = 0; i < mPanelInfos.size(); i++) {
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
@@ -72,16 +72,41 @@ public abstract class HomeFragment exten
     private boolean mIsLoaded;
 
     // On URL open listener
     protected OnUrlOpenListener mUrlOpenListener;
 
     // Helper for opening a tab in the background.
     private OnUrlOpenInBackgroundListener mUrlOpenInBackgroundListener;
 
+    protected PanelStateChangeListener mPanelStateChangeListener = null;
+
+    /**
+     * Listener to notify when a home panels' state has changed in a way that needs to be stored
+     * for history/restoration. E.g. when a folder is opened/closed in bookmarks.
+     */
+    public interface PanelStateChangeListener {
+
+        /**
+         *
+         * @param bundle Data that should be persisted, and passed to this panel if restored at a later
+         * stage.
+         */
+        void onStateChanged(Bundle bundle);
+    }
+
+    public void restoreData(Bundle data) {
+        // Do nothing
+    }
+
+    public void setPanelStateChangeListener(
+            PanelStateChangeListener mPanelStateChangeListener) {
+        this.mPanelStateChangeListener = mPanelStateChangeListener;
+    }
+
     @Override
     public void onAttach(Activity activity) {
         super.onAttach(activity);
 
         try {
             mUrlOpenListener = (OnUrlOpenListener) activity;
         } catch (ClassCastException e) {
             throw new ClassCastException(activity.toString()
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java
@@ -44,30 +44,33 @@ public class HomePager extends ViewPager
     private int mDefaultPageIndex = -1;
 
     private final OnAddPanelListener mAddPanelListener;
 
     private final HomeConfig mConfig;
     private final ConfigLoaderCallbacks mConfigLoaderCallbacks;
 
     private String mInitialPanelId;
+    private Bundle mRestoreData;
 
     // Cached original ViewPager background.
     private final Drawable mOriginalBackground;
 
     // Telemetry session for current panel.
     private TelemetryContract.Session mCurrentPanelSession;
     private String mCurrentPanelSessionSuffix;
 
     // Current load state of HomePager.
     private LoadState mLoadState;
 
     // Listens for when the current panel changes.
     private OnPanelChangeListener mPanelChangedListener;
 
+    private HomeFragment.PanelStateChangeListener mPanelStateChangeListener;
+
     // This is mostly used by UI tests to easily fetch
     // specific list views at runtime.
     public static final String LIST_TAG_HISTORY = "history";
     public static final String LIST_TAG_BOOKMARKS = "bookmarks";
     public static final String LIST_TAG_TOP_SITES = "top_sites";
     public static final String LIST_TAG_RECENT_TABS = "recent_tabs";
     public static final String LIST_TAG_BROWSER_SEARCH = "browser_search";
     public static final String LIST_TAG_REMOTE_TABS = "remote_tabs";
@@ -192,32 +195,34 @@ public class HomePager extends ViewPager
         super.addView(child, index, params);
     }
 
     /**
      * Loads and initializes the pager.
      *
      * @param fm FragmentManager for the adapter
      */
-    public void load(LoaderManager lm, FragmentManager fm, String panelId, PropertyAnimator animator) {
+    public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator) {
         mLoadState = LoadState.LOADING;
 
         mVisible = true;
         mInitialPanelId = panelId;
+        mRestoreData = restoreData;
 
         // Update the home banner message each time the HomePager is loaded.
         if (mHomeBanner != null) {
             mHomeBanner.update();
         }
 
         // Only animate on post-HC devices, when a non-null animator is given
         final boolean shouldAnimate = Versions.feature11Plus && animator != null;
 
         final HomeAdapter adapter = new HomeAdapter(mContext, fm);
         adapter.setOnAddPanelListener(mAddPanelListener);
+        adapter.setPanelStateChangeListener(mPanelStateChangeListener);
         adapter.setCanLoadHint(true);
         setAdapter(adapter);
 
         // Don't show the tabs strip until we have the
         // list of panels in place.
         mTabStrip.setVisibility(View.INVISIBLE);
 
         // Load list of panels from configuration
@@ -276,45 +281,53 @@ public class HomePager extends ViewPager
             mDecor.onPageSelected(item);
         }
 
         if (mHomeBanner != null) {
             mHomeBanner.setActive(item == mDefaultPageIndex);
         }
     }
 
+    private void restorePanelData(int item, Bundle data) {
+        ((HomeAdapter) getAdapter()).setRestoreData(item, data);
+    }
+
     /**
      * Shows a home panel. If the given panelId is null,
      * the default panel will be shown. No action will be taken if:
      *  * HomePager has not loaded yet
      *  * Panel with the given panelId cannot be found
      *
      * If you're trying to open a built-in panel, consider loading the panel url directly with
      * {@link org.mozilla.gecko.AboutPages#getURLForBuiltinPanelType(HomeConfig.PanelType)}.
      *
      * @param panelId of the home panel to be shown.
      */
-    public void showPanel(String panelId) {
+    public void showPanel(String panelId, Bundle restoreData) {
         if (!mVisible) {
             return;
         }
 
         switch (mLoadState) {
             case LOADING:
                 mInitialPanelId = panelId;
+                mRestoreData = restoreData;
                 break;
 
             case LOADED:
                 int position = mDefaultPageIndex;
                 if (panelId != null) {
                     position = ((HomeAdapter) getAdapter()).getItemPosition(panelId);
                 }
 
                 if (position > -1) {
                     setCurrentItem(position);
+                    if (restoreData != null) {
+                        restorePanelData(position, restoreData);
+                    }
                 }
                 break;
 
             default:
                 // Do nothing.
         }
     }
 
@@ -415,16 +428,20 @@ public class HomePager extends ViewPager
                 }
             }
 
             // Use the default panel if the initial panel wasn't explicitly set by the
             // load() caller, or if the initial panel is not found in the adapter.
             final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId);
             if (itemPosition > -1) {
                 setCurrentItem(itemPosition, false);
+                if (mRestoreData != null) {
+                    restorePanelData(itemPosition, mRestoreData);
+                    mRestoreData = null; // Release data since it's no longer needed
+                }
                 mInitialPanelId = null;
             } else {
                 setCurrentItem(mDefaultPageIndex, false);
             }
         }
 
         // The selection is updated asynchronously so we need to post to
         // UI thread to give the pager time to commit the new page selection
@@ -436,16 +453,25 @@ public class HomePager extends ViewPager
             }
         });
     }
 
     public void setOnPanelChangeListener(OnPanelChangeListener listener) {
        mPanelChangedListener = listener;
     }
 
+    public void setPanelStateChangelistener(HomeFragment.PanelStateChangeListener listener) {
+        mPanelStateChangeListener = listener;
+
+        HomeAdapter adapter = (HomeAdapter) getAdapter();
+        if (adapter != null) {
+            adapter.setPanelStateChangeListener(listener);
+        }
+    }
+
     /**
      * Notify listeners of newly selected panel.
      *
      * @param position of the newly selected panel
      */
     private void notifyPanelSelected(int position) {
         if (mDecor != null) {
             mDecor.onPageSelected(position);