Bug 1251362 - Part 5 - Actually show recently closed tabs when opening the smart folder. r=liuche draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Thu, 02 Jun 2016 18:08:23 +0200
changeset 376073 c4d5afcaa45f2d851a4c1d9c462b204c687b0221
parent 376072 a1806a5a1e6714707dddddeae87eca0ecd28993c
child 376074 0945b244dc42f6d3b58a28dc789465a20f14a396
push id20500
push usermozilla@buttercookie.de
push dateTue, 07 Jun 2016 11:41:20 +0000
reviewersliuche
bugs1251362
milestone50.0a1
Bug 1251362 - Part 5 - Actually show recently closed tabs when opening the smart folder. r=liuche This fills the new smart folder we've previously added with life and displays the recently closed tabs as we receive them from the session store. If we can find a sessionstore.bak file (previously the "Tabs from last time"), we also add those tabs to the bottom of the list. Most of the code for communicating with the session store and reading sessionstore.bak is adapted from the original Recent Tabs panel, however unlike the previous implementation, I've opted for a cursor-less approach of storing and retrieving the recent tabs data, since the recent tabs data isn't actually powered by a database anyway. Instead, the RecentTabsAdapter maintains two arrays for storing "Recently closed tabs" (as received through messages from the Gecko session store) and "Tabs from last time" (as read from sessionstore.bak during panel initialisation). Also, as per the other Combined History panel adapters and because we're now using a RecyclerView instead of a ListView, list item types are now determined on demand through getItemTypeForPosition() instead of precalculating them during a data update and directly storing together with the tab data items in a cursor. MozReview-Commit-ID: IpoUr9f0JBP
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
@@ -10,16 +10,17 @@ import android.support.v4.content.Contex
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.db.RemoteTab;
+import org.mozilla.gecko.home.RecentTabsAdapter.ClosedTab;
 
 public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
     private static final String LOGTAG = "CombinedHistoryItem";
 
     public CombinedHistoryItem(View view) {
         super(view);
     }
 
@@ -79,16 +80,22 @@ public abstract class CombinedHistoryIte
             pageRow.updateFromCursor(historyCursor);
         }
 
         public void bind(RemoteTab remoteTab) {
             final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
             childPageRow.setShowIcons(true);
             childPageRow.update(remoteTab.title, remoteTab.url);
         }
+
+        public void bind(ClosedTab closedTab) {
+            final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
+            childPageRow.setShowIcons(false);
+            childPageRow.update(closedTab.title, closedTab.url);
+        }
     }
 
     public static class ClientItem extends CombinedHistoryItem {
         final TextView nameView;
         final ImageView deviceTypeView;
         final TextView lastModifiedView;
         final ImageView deviceExpanded;
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -105,17 +105,17 @@ public class CombinedHistoryPanel extend
     }
 
     @Override
     public void onCreate(Bundle savedInstance) {
         super.onCreate(savedInstance);
 
         mHistoryAdapter = new CombinedHistoryAdapter(getResources());
         mClientsAdapter = new ClientsAdapter(getContext());
-        mRecentTabsAdapter = new RecentTabsAdapter();
+        mRecentTabsAdapter = new RecentTabsAdapter(getContext());
 
         mSyncStatusListener = new RemoteTabsSyncListener();
         FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
     }
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         return inflater.inflate(R.layout.home_combined_history_panel, container, false);
@@ -133,16 +133,18 @@ public class CombinedHistoryPanel extend
 
         mClientsEmptyView = view.findViewById(R.id.home_clients_empty_view);
         mHistoryEmptyView = view.findViewById(R.id.home_history_empty_view);
         mRecentTabsEmptyView = view.findViewById(R.id.home_recent_tabs_empty_view);
         setUpEmptyViews();
 
         mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
         mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
+
+        mRecentTabsAdapter.startListeningForClosedTabs();
     }
 
     private void setUpRecyclerView() {
         if (mPanelLevel == null) {
             mPanelLevel = PanelLevel.PARENT;
         }
 
         mRecyclerView.setAdapter(mPanelLevel == PanelLevel.PARENT ? mHistoryAdapter :
@@ -574,16 +576,23 @@ public class CombinedHistoryPanel extend
 
         @Override
         public void onSyncFinished() {
             mRefreshLayout.setRefreshing(false);
         }
     }
 
     @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+
+        mRecentTabsAdapter.stopListeningForClosedTabs();
+    }
+
+    @Override
     public void onDestroy() {
         super.onDestroy();
         if (mSyncStatusListener != null) {
             FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener);
             mSyncStatusListener = null;
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
@@ -1,27 +1,199 @@
 /* -*- 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.Context;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.TextView;
 
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.SessionParser;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
 
 import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType;
 
-public class RecentTabsAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
+public class RecentTabsAdapter extends RecyclerView.Adapter<CombinedHistoryItem>
+                               implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder, NativeEventListener {
     private static final String LOGTAG = "GeckoRecentTabsAdapter";
 
+    private static final int NAVIGATION_BACK_BUTTON_INDEX = 0;
+
+    // Recently closed tabs from Gecko.
+    private ClosedTab[] recentlyClosedTabs;
+
+    // "Tabs from last time".
+    private ClosedTab[] lastSessionTabs;
+
+    public static final class ClosedTab {
+        public final String url;
+        public final String title;
+        public final String data;
+
+        public ClosedTab(String url, String title, String data) {
+            this.url = url;
+            this.title = title;
+            this.data = data;
+        }
+    }
+
+    private final Context context;
+
+    public RecentTabsAdapter(Context context) {
+        this.context = context;
+        recentlyClosedTabs = new ClosedTab[0];
+        lastSessionTabs = new ClosedTab[0];
+
+        readPreviousSessionData();
+    }
+
+    public void startListeningForClosedTabs() {
+        EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
+        GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null);
+    }
+
+    public void stopListeningForClosedTabs() {
+        GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null);
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
+    }
+
+    @Override
+    public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+        final NativeJSObject[] tabs = message.getObjectArray("tabs");
+        final int length = tabs.length;
+
+        final ClosedTab[] closedTabs = new ClosedTab[length];
+        for (int i = 0; i < length; i++) {
+            final NativeJSObject tab = tabs[i];
+            closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"), tab.getObject("data").toString());
+        }
+
+        // Only modify recentlyClosedTabs on the UI thread.
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // Save some data about the old panel state, so we can be
+                // smarter about notifying the recycler view which bits changed.
+                int prevClosedTabsCount = recentlyClosedTabs.length;
+                boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
+                int prevSectionHeaderIndex = getSectionHeaderIndex();
+
+                recentlyClosedTabs = closedTabs;
+
+                // Handle the section header hiding/unhiding.
+                updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
+
+                // Update the "Recently closed" part of the tab list.
+                updateTabsList(prevClosedTabsCount, recentlyClosedTabs.length, getFirstRecentTabIndex(), getLastRecentTabIndex());
+            }
+        });
+    }
+
+    private void readPreviousSessionData() {
+        // Make sure that the start up code has had a chance to update sessionstore.bak as necessary.
+        GeckoProfile.get(context).waitForOldSessionDataProcessing();
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                final String jsonString = GeckoProfile.get(context).readSessionFile(true);
+                if (jsonString == null) {
+                    // No previous session data.
+                    return;
+                }
+
+                final List<ClosedTab> parsedTabs = new ArrayList<>();
+
+                new SessionParser() {
+                    @Override
+                    public void onTabRead(SessionTab tab) {
+                        final String url = tab.getUrl();
+
+                        // Don't show last tabs for about:home
+                        if (AboutPages.isAboutHome(url)) {
+                            return;
+                        }
+
+                        parsedTabs.add(new ClosedTab(url, tab.getTitle(), tab.getTabObject().toString()));
+                    }
+                }.parse(jsonString);
+
+                final ClosedTab[] closedTabs = parsedTabs.toArray(new ClosedTab[parsedTabs.size()]);
+
+                // Only modify lastSessionTabs on the UI thread.
+                ThreadUtils.postToUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Save some data about the old panel state, so we can be
+                        // smarter about notifying the recycler view which bits changed.
+                        int prevClosedTabsCount = lastSessionTabs.length;
+                        boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
+                        int prevSectionHeaderIndex = getSectionHeaderIndex();
+
+                        lastSessionTabs = closedTabs;
+
+                        // Handle the section header hiding/unhiding.
+                        updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
+
+                        // Update the "Tabs from last time" part of the tab list.
+                        updateTabsList(prevClosedTabsCount, lastSessionTabs.length, getFirstLastSessionTabIndex(), getLastLastSessionTabIndex());
+                    }
+                });
+            }
+        });
+    }
+
+    private void updateHeaderVisibility(boolean prevSectionHeaderVisibility, int prevSectionHeaderIndex) {
+        if (prevSectionHeaderVisibility && !isSectionHeaderVisible()) {
+            notifyItemRemoved(prevSectionHeaderIndex);
+        } else if (!prevSectionHeaderVisibility && isSectionHeaderVisible()) {
+            notifyItemInserted(getSectionHeaderIndex());
+        }
+    }
+
+    /**
+     * Updates the tab list as necessary to account for any changes in tab count in a particular data source.
+     *
+     * Since the session store only sends out full updates, we don't know for sure what has changed compared
+     * to the last data set, so we can only animate if the tab count actually changes.
+     *
+     * @param prevClosedTabsCount The previous number of closed tabs from that data source.
+     * @param closedTabsCount The current number of closed tabs contained in that data source.
+     * @param firstTabListIndex The current position of that data source's first item in the RecyclerView.
+     * @param lastTabListIndex The current position of that data source's last item in the RecyclerView.
+     */
+    private void updateTabsList(int prevClosedTabsCount, int closedTabsCount, int firstTabListIndex, int lastTabListIndex) {
+        final int closedTabsCountChange = closedTabsCount - prevClosedTabsCount;
+
+        if (closedTabsCountChange <= 0) {
+            notifyItemRangeRemoved(lastTabListIndex + 1, -closedTabsCountChange); // Remove tabs from the bottom of the list.
+            notifyItemRangeChanged(firstTabListIndex, closedTabsCount); // Update the contents of the remaining items.
+        } else { // closedTabsCountChange > 0
+            notifyItemRangeInserted(firstTabListIndex, closedTabsCountChange); // Add additional tabs at the top of the list.
+            notifyItemRangeChanged(firstTabListIndex + closedTabsCountChange, prevClosedTabsCount); // Update any previous list items.
+        }
+    }
+
     @Override
     public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
         final View view;
 
         final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
 
         switch (itemType) {
@@ -38,36 +210,94 @@ public class RecentTabsAdapter extends R
                 return new CombinedHistoryItem.HistoryItem(view);
         }
         return null;
     }
 
     @Override
     public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
         final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
+
+        switch (itemType) {
+            case SECTION_HEADER:
+                ((TextView) holder.itemView).setText(context.getString(R.string.home_closed_tabs_title2));
+                break;
+
+            case CLOSED_TAB:
+                final ClosedTab closedTab;
+                if (position <= getLastRecentTabIndex()) {
+                    closedTab = recentlyClosedTabs[position - getFirstRecentTabIndex()];
+                } else {
+                    closedTab = lastSessionTabs[position - getFirstLastSessionTabIndex()];
+                }
+                ((CombinedHistoryItem.HistoryItem) holder).bind(closedTab);
+                break;
+        }
     }
 
     @Override
     public int getItemCount() {
-        return 1;
+        int itemCount = 1; // NAVIGATION_BACK button is always visible.
+
+        if (isSectionHeaderVisible()) {
+            itemCount += 1;
+        }
+
+        itemCount += getClosedTabsCount();
+
+        return itemCount;
     }
 
     private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
-        if (position == 0) {
+        if (position == NAVIGATION_BACK_BUTTON_INDEX) {
             return ItemType.NAVIGATION_BACK;
         }
 
+        if (position == getSectionHeaderIndex() && isSectionHeaderVisible()) {
+            return ItemType.SECTION_HEADER;
+        }
+
         return ItemType.CLOSED_TAB;
     }
 
     @Override
     public int getItemViewType(int position) {
         return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
     }
 
+    public int getClosedTabsCount() {
+        return recentlyClosedTabs.length + lastSessionTabs.length;
+    }
+
+    private boolean isSectionHeaderVisible() {
+        return recentlyClosedTabs.length > 0 || lastSessionTabs.length > 0;
+    }
+
+    private int getSectionHeaderIndex() {
+        return isSectionHeaderVisible() ?
+                NAVIGATION_BACK_BUTTON_INDEX + 1 :
+                NAVIGATION_BACK_BUTTON_INDEX;
+    }
+
+    private int getFirstRecentTabIndex() {
+        return getSectionHeaderIndex() + 1;
+    }
+
+    private int getLastRecentTabIndex() {
+        return getSectionHeaderIndex() + recentlyClosedTabs.length;
+    }
+
+    private int getFirstLastSessionTabIndex() {
+        return getLastRecentTabIndex() + 1;
+    }
+
+    private int getLastLastSessionTabIndex() {
+        return getLastRecentTabIndex() + lastSessionTabs.length;
+    }
+
     @Override
     public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
         final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
         final HomeContextMenuInfo info;
 
         return null;
     }
 }