Bug 1277978 - Part 1 - Hide Recent Tabs smart folder if there aren't any closed tabs to be shown. r=liuche,rnewman
authorJan Henning <jh+bugzilla@buttercookie.de>
Sun, 04 Sep 2016 15:16:20 +0200
changeset 313449 a93820dea9baa82506443c2a649022128e9ddc34
parent 313448 2329e646600993d7603ede43851faaf8ea8c8da0
child 313450 1ab243749c5ee87ab0683523c01de1728dfd7019
push id30685
push userphilringnalda@gmail.com
push dateSun, 11 Sep 2016 05:38:20 +0000
treeherdermozilla-central@7e873393cc11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersliuche, rnewman
bugs1277978
milestone51.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 1277978 - Part 1 - Hide Recent Tabs smart folder if there aren't any closed tabs to be shown. r=liuche,rnewman This involves making the number of visible smart folders dynamic, so the history adapter can properly display its contents. MozReview-Commit-ID: 6b4V6IHB7BE
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -1,29 +1,31 @@
 /* -*- 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 android.content.res.Resources;
+import android.support.annotation.UiThread;
+import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 
 import android.database.Cursor;
 import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.util.ThreadUtils;
 
 public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
     private static final int RECENT_TABS_SMARTFOLDER_INDEX = 0;
-    private static final int SYNCED_DEVICES_SMARTFOLDER_INDEX = 1;
 
     // Array for the time ranges in milliseconds covered by each section.
     static final HistorySectionsHelper.SectionDateRange[] sectionDateRangeArray = new HistorySectionsHelper.SectionDateRange[SectionHeader.values().length];
 
     // Semantic names for the time covered by each section
     public enum SectionHeader {
         TODAY,
         YESTERDAY,
@@ -38,66 +40,138 @@ public class CombinedHistoryAdapter exte
     }
 
     private Cursor historyCursor;
     private DevicesUpdateHandler devicesUpdateHandler;
     private int deviceCount = 0;
     private RecentTabsUpdateHandler recentTabsUpdateHandler;
     private int recentTabsCount = 0;
 
+    private LinearLayoutManager linearLayoutManager; // Only used on the UI thread, so no need to be volatile.
+
     // We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
     private final SparseArray<SectionHeader> sectionHeaders;
 
     public CombinedHistoryAdapter(Resources resources) {
         super();
         sectionHeaders = new SparseArray<>();
         HistorySectionsHelper.updateRecentSectionOffset(resources, sectionDateRangeArray);
         this.setHasStableIds(true);
     }
 
+    @UiThread
+    public void setLinearLayoutManager(LinearLayoutManager linearLayoutManager) {
+        this.linearLayoutManager = linearLayoutManager;
+    }
+
     public void setHistory(Cursor history) {
         historyCursor = history;
         populateSectionHeaders(historyCursor, sectionHeaders);
         notifyDataSetChanged();
     }
 
     public interface DevicesUpdateHandler {
         void onDeviceCountUpdated(int count);
     }
 
     public DevicesUpdateHandler getDeviceUpdateHandler() {
         if (devicesUpdateHandler == null) {
             devicesUpdateHandler = new DevicesUpdateHandler() {
                 @Override
                 public void onDeviceCountUpdated(int count) {
                     deviceCount = count;
-                    notifyItemChanged(SYNCED_DEVICES_SMARTFOLDER_INDEX);
+                    notifyItemChanged(getSyncedDevicesSmartFolderIndex());
                 }
             };
         }
         return devicesUpdateHandler;
     }
 
     public interface RecentTabsUpdateHandler {
         void onRecentTabsCountUpdated(int count);
     }
 
     public RecentTabsUpdateHandler getRecentTabsUpdateHandler() {
-        if (recentTabsUpdateHandler == null) {
-            recentTabsUpdateHandler = new RecentTabsUpdateHandler() {
-                @Override
-                public void onRecentTabsCountUpdated(int count) {
-                    recentTabsCount = count;
-                    notifyItemChanged(RECENT_TABS_SMARTFOLDER_INDEX);
-                }
-            };
+        if (recentTabsUpdateHandler != null) {
+            return recentTabsUpdateHandler;
         }
+
+        recentTabsUpdateHandler = new RecentTabsUpdateHandler() {
+            @Override
+            public void onRecentTabsCountUpdated(final int count) {
+                // Now that other items can move around depending on the visibility of the
+                // Recent Tabs folder, only update the recentTabsCount on the UI thread.
+                ThreadUtils.postToUiThread(new Runnable() {
+                    @UiThread
+                    @Override
+                    public void run() {
+                        final boolean prevFolderVisibility = isRecentTabsFolderVisible();
+                        recentTabsCount = count;
+                        final boolean folderVisible = isRecentTabsFolderVisible();
+
+                        if (prevFolderVisibility == folderVisible) {
+                            if (prevFolderVisibility) {
+                                notifyItemChanged(RECENT_TABS_SMARTFOLDER_INDEX);
+                            }
+                            return;
+                        }
+
+                        // If the Recent Tabs smart folder has become hidden/unhidden,
+                        // we need to recalculate the history section header positions.
+                        populateSectionHeaders(historyCursor, sectionHeaders);
+
+                        if (folderVisible) {
+                            int scrollPos = -1;
+                            if (linearLayoutManager != null) {
+                                scrollPos = linearLayoutManager.findFirstCompletelyVisibleItemPosition();
+                            }
+
+                            notifyItemInserted(RECENT_TABS_SMARTFOLDER_INDEX);
+                            // If the list exceeds the display height and we want to show the new
+                            // item inserted at position 0, we need to scroll up manually
+                            // (see https://code.google.com/p/android/issues/detail?id=174227#c2).
+                            // However we only do this if our current scroll position is at the
+                            // top of the list.
+                            if (linearLayoutManager != null && scrollPos == 0) {
+                                linearLayoutManager.scrollToPosition(0);
+                            }
+                        } else {
+                            notifyItemRemoved(RECENT_TABS_SMARTFOLDER_INDEX);
+                        }
+                    }
+                });
+            }
+        };
         return recentTabsUpdateHandler;
     }
 
+    @UiThread
+    private boolean isRecentTabsFolderVisible() {
+        return recentTabsCount > 0;
+    }
+
+    @UiThread
+    // Number of smart folders for determining practical empty state.
+    public int getNumVisibleSmartFolders() {
+        int visibleFolders = 1; // Synced devices folder is always visible.
+
+        if (isRecentTabsFolderVisible()) {
+            visibleFolders += 1;
+        }
+
+        return visibleFolders;
+    }
+
+    @UiThread
+    private int getSyncedDevicesSmartFolderIndex() {
+        return isRecentTabsFolderVisible() ?
+                RECENT_TABS_SMARTFOLDER_INDEX + 1 :
+                RECENT_TABS_SMARTFOLDER_INDEX;
+    }
+
     @Override
     public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
         final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
         final View view;
 
         final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
 
         switch (itemType) {
@@ -150,57 +224,62 @@ public class CombinedHistoryAdapter exte
      *
      * The type is not strictly necessary and could be fetched from <code>getItemTypeForPosition</code>,
      * but is used for explicitness.
      *
      * @param type ItemType of the item
      * @param position position in the adapter
      * @return position of the item in the data structure
      */
+    @UiThread
     private int transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType type, int position) {
         if (type == CombinedHistoryItem.ItemType.SECTION_HEADER) {
             return position;
         } else if (type == CombinedHistoryItem.ItemType.HISTORY) {
-            return position - getHeadersBefore(position) - CombinedHistoryPanel.NUM_SMART_FOLDERS;
+            return position - getHeadersBefore(position) - getNumVisibleSmartFolders();
         } else {
             return position;
         }
     }
 
+    @UiThread
     private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
-        if (position == RECENT_TABS_SMARTFOLDER_INDEX) {
+        if (position == RECENT_TABS_SMARTFOLDER_INDEX && isRecentTabsFolderVisible()) {
             return CombinedHistoryItem.ItemType.RECENT_TABS;
         }
-        if (position == SYNCED_DEVICES_SMARTFOLDER_INDEX) {
+        if (position == getSyncedDevicesSmartFolderIndex()) {
             return CombinedHistoryItem.ItemType.SYNCED_DEVICES;
         }
         final int sectionPosition = transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.SECTION_HEADER, position);
         if (sectionHeaders.get(sectionPosition) != null) {
             return CombinedHistoryItem.ItemType.SECTION_HEADER;
         }
         return CombinedHistoryItem.ItemType.HISTORY;
     }
 
+    @UiThread
     @Override
     public int getItemViewType(int position) {
         return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
     }
 
+    @UiThread
     @Override
     public int getItemCount() {
         final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
-        return historySize + sectionHeaders.size() + CombinedHistoryPanel.NUM_SMART_FOLDERS;
+        return historySize + sectionHeaders.size() + getNumVisibleSmartFolders();
     }
 
     /**
      * Returns stable ID for each position. Data behind historyCursor is a sorted Combined view.
      *
      * @param position view item position for which to generate a stable ID
      * @return stable ID for given position
      */
+    @UiThread
     @Override
     public long getItemId(int position) {
         // Two randomly selected large primes used to generate non-clashing IDs.
         final long PRIME_BOOKMARKS = 32416189867L;
         final long PRIME_SECTION_HEADERS = 32416187737L;
 
         // RecyclerView.NO_ID is -1, so let's start from -2 for our hard-coded IDs.
         final int RECENT_TABS_ID = -2;
@@ -239,33 +318,36 @@ public class CombinedHistoryAdapter exte
     }
 
     /**
      * Add only the SectionHeaders that have history items within their range to a SparseArray, where the
      * array index is the position of the header in the history-only (no clients) ordering.
      * @param c data Cursor
      * @param sparseArray SparseArray to populate
      */
-    private static void populateSectionHeaders(Cursor c, SparseArray<SectionHeader> sparseArray) {
+    @UiThread
+    private void populateSectionHeaders(Cursor c, SparseArray<SectionHeader> sparseArray) {
+        ThreadUtils.assertOnUiThread();
+
         sparseArray.clear();
 
         if (c == null || !c.moveToFirst()) {
             return;
         }
 
         SectionHeader section = null;
 
         do {
             final int historyPosition = c.getPosition();
             final long visitTime = c.getLong(c.getColumnIndexOrThrow(BrowserContract.History.DATE_LAST_VISITED));
             final SectionHeader itemSection = getSectionFromTime(visitTime);
 
             if (section != itemSection) {
                 section = itemSection;
-                sparseArray.append(historyPosition + sparseArray.size() + CombinedHistoryPanel.NUM_SMART_FOLDERS, section);
+                sparseArray.append(historyPosition + sparseArray.size() + getNumVisibleSmartFolders(), section);
             }
 
             if (section == SectionHeader.OLDER_THAN_SIX_MONTHS) {
                 break;
             }
         } while (c.moveToNext());
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -8,16 +8,17 @@ package org.mozilla.gecko.home;
 import android.accounts.Account;
 import android.app.AlertDialog;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.support.annotation.UiThread;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.content.Loader;
 import android.support.v4.widget.SwipeRefreshLayout;
 import android.support.v7.widget.DefaultItemAnimator;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.text.SpannableStringBuilder;
 import android.text.TextPaint;
@@ -63,19 +64,16 @@ public class CombinedHistoryPanel extend
     private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "clients", "tabs" };
     private final int LOADER_ID_HISTORY = 0;
     private final int LOADER_ID_REMOTE = 1;
 
     // String placeholders to mark formatting.
     private final static String FORMAT_S1 = "%1$s";
     private final static String FORMAT_S2 = "%2$s";
 
-    // Number of smart folders for determining practical empty state.
-    public static final int NUM_SMART_FOLDERS = 2;
-
     private CombinedHistoryRecyclerView mRecyclerView;
     private CombinedHistoryAdapter mHistoryAdapter;
     private ClientsAdapter mClientsAdapter;
     private RecentTabsAdapter mRecentTabsAdapter;
     private CursorLoaderCallbacks mCursorLoaderCallbacks;
 
     private Bundle mSavedRestoreBundle;
 
@@ -124,16 +122,17 @@ public class CombinedHistoryPanel extend
         FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
     }
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         return inflater.inflate(R.layout.home_combined_history_panel, container, false);
     }
 
+    @UiThread
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
 
         mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view);
         setUpRecyclerView();
 
         mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
@@ -152,30 +151,32 @@ public class CombinedHistoryPanel extend
         mRecentTabsAdapter.startListeningForHistorySanitize();
 
         if (mSavedRestoreBundle != null) {
             setPanelStateFromBundle(mSavedRestoreBundle);
             mSavedRestoreBundle = null;
         }
     }
 
+    @UiThread
     private void setUpRecyclerView() {
         if (mPanelLevel == null) {
             mPanelLevel = PanelLevel.PARENT;
         }
 
         mRecyclerView.setAdapter(mPanelLevel == PanelLevel.PARENT ? mHistoryAdapter :
                 mPanelLevel == PanelLevel.CHILD_SYNC ? mClientsAdapter : mRecentTabsAdapter);
 
         final RecyclerView.ItemAnimator animator = new DefaultItemAnimator();
         animator.setAddDuration(100);
         animator.setChangeDuration(100);
         animator.setMoveDuration(100);
         animator.setRemoveDuration(100);
         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+        mHistoryAdapter.setLinearLayoutManager((LinearLayoutManager) mRecyclerView.getLayoutManager());
         mRecyclerView.setItemAnimator(animator);
         mRecyclerView.addItemDecoration(new HistoryDividerItemDecoration(getContext()));
         mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener);
         mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener());
         mRecyclerView.setHiddenClientsDialogBuilder(new HiddenClientsHelper());
         mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
             @Override
             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
@@ -389,17 +390,17 @@ public class CombinedHistoryPanel extend
             return true;
         }
     }
 
     private void updateButtonFromLevel() {
         switch (mPanelLevel) {
             case PARENT:
                 final boolean historyRestricted = !Restrictions.isAllowed(getActivity(), Restrictable.CLEAR_HISTORY);
-                if (historyRestricted || mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS) {
+                if (historyRestricted || mHistoryAdapter.getItemCount() == mHistoryAdapter.getNumVisibleSmartFolders()) {
                     mPanelFooterButton.setVisibility(View.GONE);
                 } else {
                     mPanelFooterButton.setText(R.string.home_clear_history_button);
                     mPanelFooterButton.setVisibility(View.VISIBLE);
                 }
                 break;
             case CHILD_RECENT_TABS:
                 if (mRecentTabsAdapter.getClosedTabsCount() > 1) {
@@ -460,17 +461,17 @@ public class CombinedHistoryPanel extend
     }
 
     private void updateEmptyView() {
         boolean showEmptyHistoryView = false;
         boolean showEmptyClientsView = false;
         boolean showEmptyRecentTabsView = false;
         switch (mPanelLevel) {
             case PARENT:
-                showEmptyHistoryView = mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS;
+                showEmptyHistoryView = mHistoryAdapter.getItemCount() == mHistoryAdapter.getNumVisibleSmartFolders();
                 break;
 
             case CHILD_SYNC:
                 showEmptyClientsView = mClientsAdapter.getItemCount() == 1;
                 break;
 
             case CHILD_RECENT_TABS:
                 showEmptyRecentTabsView = mRecentTabsAdapter.getClosedTabsCount() == 0;