Bug 1277978 - Part 1 - Hide Recent Tabs smart folder if there aren't any closed tabs to be shown. r=liuche r=rnewman draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Sun, 04 Sep 2016 15:16:20 +0200
changeset 412352 fb65db89dbc47028f21e5c92b1e8e51ed9bd10b4
parent 412247 6b82ccf8c3fedee10688b4078882222cf231cb33
child 412353 b5cd02ef0dc8d538bf9fc3402fb5ae44cf95fdf6
push id29155
push usermozilla@buttercookie.de
push dateSat, 10 Sep 2016 11:55:24 +0000
reviewersliuche, rnewman
bugs1277978
milestone51.0a1
Bug 1277978 - Part 1 - Hide Recent Tabs smart folder if there aren't any closed tabs to be shown. r=liuche r=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;