diff options
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java')
-rwxr-xr-x | mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java | 454 |
1 files changed, 454 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java new file mode 100755 index 000000000..3091f77da --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java @@ -0,0 +1,454 @@ +/* -*- 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.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.AboutPages; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.GeckoApp; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.R; +import org.mozilla.gecko.SessionParser; +import org.mozilla.gecko.home.CombinedHistoryAdapter.RecentTabsUpdateHandler; +import org.mozilla.gecko.home.CombinedHistoryPanel.PanelStateUpdateHandler; +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; +import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS; + +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; + + private static final String TELEMETRY_EXTRA_LAST_TIME = "recent_tabs_last_time"; + private static final String TELEMETRY_EXTRA_RECENTLY_CLOSED = "recent_closed_tabs"; + private static final String TELEMETRY_EXTRA_MIXED = "recent_tabs_mixed"; + + // Recently closed tabs from Gecko. + private ClosedTab[] recentlyClosedTabs; + private boolean recentlyClosedTabsReceived = false; + + // "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; + private final RecentTabsUpdateHandler recentTabsUpdateHandler; + private final PanelStateUpdateHandler panelStateUpdateHandler; + + public RecentTabsAdapter(Context context, + RecentTabsUpdateHandler recentTabsUpdateHandler, + PanelStateUpdateHandler panelStateUpdateHandler) { + this.context = context; + this.recentTabsUpdateHandler = recentTabsUpdateHandler; + this.panelStateUpdateHandler = panelStateUpdateHandler; + 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"); + recentlyClosedTabsReceived = false; + } + + public void startListeningForHistorySanitize() { + EventDispatcher.getInstance().registerGeckoThreadListener(this, "Sanitize:Finished"); + } + + public void stopListeningForHistorySanitize() { + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Sanitize:Finished"); + } + + @Override + public void handleMessage(String event, NativeJSObject message, EventCallback callback) { + switch (event) { + case "ClosedTabs:Data": + updateRecentlyClosedTabs(message); + break; + case "Sanitize:Finished": + clearLastSessionData(); + break; + } + } + + private void updateRecentlyClosedTabs(NativeJSObject message) { + 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; + recentlyClosedTabsReceived = true; + recentTabsUpdateHandler.onRecentTabsCountUpdated( + getClosedTabsCount(), recentlyClosedTabsReceived); + panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS); + + // 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() { + // If we happen to initialise before GeckoApp, waiting on either the main or the background + // thread can lead to a deadlock, so we have to run on a separate thread instead. + final Thread parseThread = new Thread(new Runnable() { + @Override + public void run() { + // Make sure that the start up code has had a chance to update sessionstore.old as necessary. + GeckoProfile.get(context).waitForOldSessionDataProcessing(); + + final String jsonString = GeckoProfile.get(context).readPreviousSessionFile(); + 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; + recentTabsUpdateHandler.onRecentTabsCountUpdated( + getClosedTabsCount(), recentlyClosedTabsReceived); + panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS); + + // 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()); + } + }); + } + }, "LastSessionTabsThread"); + + parseThread.start(); + } + + private void clearLastSessionData() { + final ClosedTab[] emptyLastSessionTabs = new ClosedTab[0]; + + // Only modify mLastSessionTabs 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 = emptyLastSessionTabs; + recentTabsUpdateHandler.onRecentTabsCountUpdated( + getClosedTabsCount(), recentlyClosedTabsReceived); + panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS); + + // Handle the section header hiding. + updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex); + + // Handle the "tabs from last time" being cleared. + if (prevClosedTabsCount > 0) { + notifyItemRangeRemoved(getFirstLastSessionTabIndex(), prevClosedTabsCount); + } + } + }); + } + + 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. + } + } + + public String restoreTabFromPosition(int position) { + final List<String> dataList = new ArrayList<>(1); + dataList.add(getClosedTabForPosition(position).data); + + final String telemetryExtra = + position > getLastRecentTabIndex() ? TELEMETRY_EXTRA_LAST_TIME : TELEMETRY_EXTRA_RECENTLY_CLOSED; + + restoreSessionWithHistory(dataList); + + return telemetryExtra; + } + + public String restoreAllTabs() { + if (recentlyClosedTabs.length == 0 && lastSessionTabs.length == 0) { + return null; + } + + final List<String> dataList = new ArrayList<>(getClosedTabsCount()); + addTabDataToList(dataList, recentlyClosedTabs); + addTabDataToList(dataList, lastSessionTabs); + + final String telemetryExtra = recentlyClosedTabs.length > 0 && lastSessionTabs.length > 0 ? TELEMETRY_EXTRA_MIXED : + recentlyClosedTabs.length > 0 ? TELEMETRY_EXTRA_RECENTLY_CLOSED : TELEMETRY_EXTRA_LAST_TIME; + + restoreSessionWithHistory(dataList); + + return telemetryExtra; + } + + private void addTabDataToList(List<String> dataList, ClosedTab[] closedTabs) { + for (ClosedTab closedTab : closedTabs) { + dataList.add(closedTab.data); + } + } + + private static void restoreSessionWithHistory(List<String> dataList) { + final JSONObject json = new JSONObject(); + try { + json.put("tabs", new JSONArray(dataList)); + } catch (JSONException e) { + Log.e(LOGTAG, "JSON error", e); + } + + GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString()); + } + + @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) { + case NAVIGATION_BACK: + view = inflater.inflate(R.layout.home_combined_back_item, parent, false); + return new CombinedHistoryItem.HistoryItem(view); + + case SECTION_HEADER: + view = inflater.inflate(R.layout.home_header_row, parent, false); + return new CombinedHistoryItem.BasicItem(view); + + case CLOSED_TAB: + view = inflater.inflate(R.layout.home_item_row, parent, false); + 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 = getClosedTabForPosition(position); + ((CombinedHistoryItem.HistoryItem) holder).bind(closedTab); + break; + } + } + + @Override + public int getItemCount() { + 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 == 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; + } + + /** + * Get the closed tab corresponding to a RecyclerView list item. + * + * The Recent Tab folder combines two data sources, so if we want to get the ClosedTab object + * behind a certain list item, we need to route this request to the corresponding data source + * and also transform the global list position into a local array index. + */ + private ClosedTab getClosedTabForPosition(int position) { + final ClosedTab closedTab; + if (position <= getLastRecentTabIndex()) { // Upper part of the list is "Recently closed tabs". + closedTab = recentlyClosedTabs[position - getFirstRecentTabIndex()]; + } else { // Lower part is "Tabs from last time". + closedTab = lastSessionTabs[position - getFirstLastSessionTabIndex()]; + } + + return closedTab; + } + + @Override + public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) { + final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position); + final HomeContextMenuInfo info; + + switch (itemType) { + case CLOSED_TAB: + info = new HomeContextMenuInfo(view, position, -1); + ClosedTab closedTab = getClosedTabForPosition(position); + return populateChildInfoFromTab(info, closedTab); + } + + return null; + } + + protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, ClosedTab tab) { + info.url = tab.url; + info.title = tab.title; + return info; + } +} |