// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.ntp;
import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.LruCache;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.IntDef;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.recent_tabs.ForeignSessionHelper.ForeignSession;
import org.chromium.chrome.browser.recent_tabs.ForeignSessionHelper.ForeignSessionTab;
import org.chromium.chrome.browser.recent_tabs.ForeignSessionHelper.ForeignSessionWindow;
import org.chromium.chrome.browser.signin.LegacySyncPromoView;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper.DefaultFaviconHelper;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper.FaviconImageCallback;
import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
import org.chromium.chrome.browser.ui.signin.SyncPromoController.SyncPromoState;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.signin.metrics.SigninAccessPoint;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.mojom.WindowOpenDisposition;
import org.chromium.url.GURL;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Row adapter for presenting recently closed tabs, synced tabs from other devices, the sync or
* sign in promo, and currently open tabs (only in document mode) in a grouped list view.
*/
public class RecentTabsRowAdapter extends BaseExpandableListAdapter {
private static final int MAX_NUM_FAVICONS_TO_CACHE = 128;
@IntDef({
ChildType.NONE,
ChildType.DEFAULT_CONTENT,
ChildType.PERSONALIZED_SIGNIN_PROMO,
ChildType.PERSONALIZED_SYNC_PROMO,
ChildType.SYNC_PROMO
})
@Retention(RetentionPolicy.SOURCE)
private @interface ChildType {
// Values should be enumerated from 0 and can't have gaps.
int NONE = 0;
int DEFAULT_CONTENT = 1;
int PERSONALIZED_SIGNIN_PROMO = 2;
int PERSONALIZED_SYNC_PROMO = 3;
int SYNC_PROMO = 4;
/** Number of entries. */
int NUM_ENTRIES = 5;
}
@IntDef({GroupType.CONTENT, GroupType.VISIBLE_SEPARATOR, GroupType.INVISIBLE_SEPARATOR})
@Retention(RetentionPolicy.SOURCE)
private @interface GroupType {
// Values should be enumerated from 0 and can't have gaps.
int CONTENT = 0;
int VISIBLE_SEPARATOR = 1;
int INVISIBLE_SEPARATOR = 2;
/** Number of entries. */
int NUM_ENTRIES = 3;
}
// Values from the OtherSessionsActions enum in histograms.xml; do not change these values or
// histograms will be broken.
@IntDef({
OtherSessionsActions.MENU_INITIALIZED,
OtherSessionsActions.LINK_CLICKED,
OtherSessionsActions.COLLAPSE_SESSION,
OtherSessionsActions.EXPAND_SESSION,
OtherSessionsActions.OPEN_ALL,
OtherSessionsActions.HAS_FOREIGN_DATA,
OtherSessionsActions.HIDE_FOR_NOW
})
@Retention(RetentionPolicy.SOURCE)
private @interface OtherSessionsActions {
int MENU_INITIALIZED = 0;
int LINK_CLICKED = 2;
int COLLAPSE_SESSION = 6;
int EXPAND_SESSION = 7;
int OPEN_ALL = 8;
int HAS_FOREIGN_DATA = 9;
int HIDE_FOR_NOW = 10;
int NUM_ENTRIES = 11;
}
@IntDef({FaviconLocality.LOCAL, FaviconLocality.FOREIGN})
@Retention(RetentionPolicy.SOURCE)
private @interface FaviconLocality {
int LOCAL = 0;
int FOREIGN = 1;
int NUM_ENTRIES = 2;
}
private final Activity mActivity;
private final List<Group> mGroups;
private final DefaultFaviconHelper mDefaultFaviconHelper;
private final RecentTabsManager mRecentTabsManager;
private final RecentlyClosedTabsGroup mRecentlyClosedTabsGroup = new RecentlyClosedTabsGroup();
private final SeparatorGroup mVisibleSeparatorGroup = new SeparatorGroup(true);
private final SeparatorGroup mInvisibleSeparatorGroup = new SeparatorGroup(false);
private final Map<Integer, FaviconCache> mFaviconCaches =
new ArrayMap<>(FaviconLocality.NUM_ENTRIES);
private final int mFaviconSize;
private boolean mHasForeignDataRecorded;
private RoundedIconGenerator mIconGenerator;
/**
* A generic group of objects to be shown in the RecentTabsRowAdapter, such as the list of
* recently closed tabs.
*/
abstract class Group {
/**
* @return The type of group: GroupType.CONTENT or GroupType.SEPARATOR.
*/
abstract @GroupType int getGroupType();
/**
* @return The number of children in this group.
*/
abstract int getChildrenCount();
/**
* @return The child type.
*/
abstract @ChildType int getChildType();
/**
* @param childPosition The position for which to return the child.
* @return The child at the position childPosition.
*/
Object getChild(int childPosition) {
return null;
}
/**
* Returns the view corresponding to the child view at a given position.
*
* @param childPosition The position of the child.
* @param isLastChild Whether this child is the last one.
* @param convertView The re-usable child view (may be null).
* @param parent The parent view group.
*
* @return The view corresponding to the child.
*/
View getChildView(
int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
View childView = convertView;
if (childView == null) {
LayoutInflater inflater = LayoutInflater.from(mActivity);
childView = inflater.inflate(R.layout.recent_tabs_list_item, parent, false);
ViewHolder viewHolder = new ViewHolder();
viewHolder.textView = childView.findViewById(R.id.title_row);
viewHolder.domainView = childView.findViewById(R.id.domain_row);
viewHolder.imageView = childView.findViewById(R.id.recent_tabs_favicon);
viewHolder.imageView.setBackgroundResource(R.drawable.list_item_icon_modern_bg);
viewHolder.itemLayout = childView.findViewById(R.id.recent_tabs_list_item_layout);
childView.setTag(viewHolder);
}
ViewHolder viewHolder = (ViewHolder) childView.getTag();
configureChildView(childPosition, viewHolder);
return childView;
}
/**
* Configures a view inflated from recent_tabs_list_item.xml to display information about
* a child in this group.
*
* @param childPosition The position of the child within this group.
* @param viewHolder The ViewHolder with references to pieces of the view.
*/
void configureChildView(int childPosition, ViewHolder viewHolder) {}
/**
* Returns the view corresponding to this group.
*
* @param isExpanded Whether the group is expanded.
* @param convertView The re-usable group view (may be null).
* @param parent The parent view group.
*
* @return The view corresponding to the group.
*/
public View getGroupView(boolean isExpanded, View convertView, ViewGroup parent) {
RecentTabsGroupView groupView = (RecentTabsGroupView) convertView;
if (groupView == null) {
groupView =
(RecentTabsGroupView)
LayoutInflater.from(mActivity)
.inflate(R.layout.recent_tabs_group_item, parent, false);
}
configureGroupView(groupView, isExpanded);
return groupView;
}
/**
* Configures an RecentTabsGroupView to display the header of this group.
* @param groupView The RecentTabsGroupView to configure.
* @param isExpanded Whether the view is currently expanded.
*/
abstract void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded);
/** Sets whether this group is collapsed (i.e. whether only the header is visible). */
abstract void setCollapsed(boolean isCollapsed);
/**
* @return Whether this group is collapsed.
*/
abstract boolean isCollapsed();
/**
* Called when a child item is clicked.
* @param childPosition The position of the child in the group.
* @return Whether the click was handled.
*/
boolean onChildClick(int childPosition) {
return false;
}
/**
* Called when the context menu for the group view is being built.
* @param menu The context menu being built.
* @param activity The current activity.
*/
void onCreateContextMenuForGroup(ContextMenu menu, Activity activity) {}
/**
* Called when a context menu for one of the child views is being built.
* @param childPosition The position of the child in the group.
* @param menu The context menu being built.
* @param activity The current activity.
*/
void onCreateContextMenuForChild(int childPosition, ContextMenu menu, Activity activity) {}
}
/** A group containing all the tabs associated with a foreign session from a synced device. */
class ForeignSessionGroup extends Group {
private final ForeignSession mForeignSession;
ForeignSessionGroup(ForeignSession foreignSession) {
mForeignSession = foreignSession;
}
@Override
public @GroupType int getGroupType() {
return GroupType.CONTENT;
}
@Override
public int getChildrenCount() {
int count = 0;
for (ForeignSessionWindow window : mForeignSession.windows) {
count += window.tabs.size();
}
return count;
}
@Override
public @ChildType int getChildType() {
return ChildType.DEFAULT_CONTENT;
}
@Override
public ForeignSessionTab getChild(int childPosition) {
for (ForeignSessionWindow window : mForeignSession.windows) {
if (childPosition < window.tabs.size()) {
return window.tabs.get(childPosition);
}
childPosition -= window.tabs.size();
}
assert false;
return null;
}
@Override
public void configureChildView(int childPosition, ViewHolder viewHolder) {
ForeignSessionTab sessionTab = getChild(childPosition);
String url = sessionTab.url.getSpec();
String text = TextUtils.isEmpty(sessionTab.title) ? url : sessionTab.title;
viewHolder.textView.setText(text);
String domain = UrlUtilities.getDomainAndRegistry(url, false);
if (!TextUtils.isEmpty(domain)) {
viewHolder.domainView.setText(domain);
viewHolder.domainView.setVisibility(View.VISIBLE);
} else {
viewHolder.domainView.setText("");
viewHolder.domainView.setVisibility(View.GONE);
}
loadFavicon(viewHolder, sessionTab.url, FaviconLocality.FOREIGN);
}
@Override
public void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded) {
groupView.configureForForeignSession(mForeignSession, isExpanded);
}
@Override
public void setCollapsed(boolean isCollapsed) {
if (isCollapsed) {
RecordHistogram.recordEnumeratedHistogram(
"HistoryPage.OtherDevicesMenu",
OtherSessionsActions.COLLAPSE_SESSION,
OtherSessionsActions.NUM_ENTRIES);
} else {
RecordHistogram.recordEnumeratedHistogram(
"HistoryPage.OtherDevicesMenu",
OtherSessionsActions.EXPAND_SESSION,
OtherSessionsActions.NUM_ENTRIES);
}
mRecentTabsManager.setForeignSessionCollapsed(mForeignSession, isCollapsed);
}
@Override
public boolean isCollapsed() {
return mRecentTabsManager.getForeignSessionCollapsed(mForeignSession);
}
@Override
public boolean onChildClick(int childPosition) {
RecordHistogram.recordEnumeratedHistogram(
"HistoryPage.OtherDevicesMenu",
OtherSessionsActions.LINK_CLICKED,
OtherSessionsActions.NUM_ENTRIES);
ForeignSessionTab foreignSessionTab = getChild(childPosition);
mRecentTabsManager.openForeignSessionTab(
mForeignSession, foreignSessionTab, WindowOpenDisposition.CURRENT_TAB);
return true;
}
@Override
public void onCreateContextMenuForGroup(ContextMenu menu, Activity activity) {
menu.add(R.string.recent_tabs_open_all_menu_option)
.setOnMenuItemClickListener(
item -> {
RecordHistogram.recordEnumeratedHistogram(
"HistoryPage.OtherDevicesMenu",
OtherSessionsActions.OPEN_ALL,
OtherSessionsActions.NUM_ENTRIES);
openAllTabs();
return true;
});
menu.add(R.string.recent_tabs_hide_menu_option)
.setOnMenuItemClickListener(
item -> {
RecordHistogram.recordEnumeratedHistogram(
"HistoryPage.OtherDevicesMenu",
OtherSessionsActions.HIDE_FOR_NOW,
OtherSessionsActions.NUM_ENTRIES);
mRecentTabsManager.deleteForeignSession(mForeignSession);
return true;
});
}
@Override
public void onCreateContextMenuForChild(
int childPosition, ContextMenu menu, Activity activity) {
final ForeignSessionTab foreignSessionTab = getChild(childPosition);
OnMenuItemClickListener listener =
item -> {
mRecentTabsManager.openForeignSessionTab(
mForeignSession,
foreignSessionTab,
WindowOpenDisposition.NEW_BACKGROUND_TAB);
return true;
};
menu.add(R.string.contextmenu_open_in_new_tab).setOnMenuItemClickListener(listener);
}
private void openAllTabs() {
ForeignSessionTab firstTab = null;
for (ForeignSessionWindow window : mForeignSession.windows) {
for (ForeignSessionTab tab : window.tabs) {
if (firstTab == null) {
firstTab = tab;
} else {
mRecentTabsManager.openForeignSessionTab(
mForeignSession, tab, WindowOpenDisposition.NEW_BACKGROUND_TAB);
}
}
}
// Open the first tab last because calls to openForeignSessionTab after one for
// CURRENT_TAB are ignored.
if (firstTab != null) {
mRecentTabsManager.openForeignSessionTab(
mForeignSession, firstTab, WindowOpenDisposition.CURRENT_TAB);
}
}
}
/** A base group for promos. */
private abstract class PromoGroup extends Group {
@Override
@GroupType
int getGroupType() {
return GroupType.CONTENT;
}
@Override
int getChildrenCount() {
return 1;
}
@Override
void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded) {
groupView.configureForPromo(isExpanded);
}
@Override
void setCollapsed(boolean isCollapsed) {
mRecentTabsManager.setPromoCollapsed(isCollapsed);
}
@Override
boolean isCollapsed() {
return mRecentTabsManager.isPromoCollapsed();
}
}
/** A group containing the personalized sync promo. */
class PersonalizedSyncPromoGroup extends PromoGroup {
private final @ChildType int mChildType;
PersonalizedSyncPromoGroup(@ChildType int childType) {
assert childType == ChildType.PERSONALIZED_SIGNIN_PROMO
|| childType == ChildType.PERSONALIZED_SYNC_PROMO
: "Unsupported child type:" + childType;
mChildType = childType;
}
@Override
@ChildType
int getChildType() {
return mChildType;
}
@Override
View getChildView(
int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
convertView =
layoutInflater.inflate(R.layout.sync_promo_view_recent_tabs, parent, false);
}
mRecentTabsManager.setUpSyncPromoView(
convertView.findViewById(R.id.signin_promo_view_container));
return convertView;
}
}
/** A group containing the sync promo. */
class SyncPromoGroup extends PromoGroup {
@Override
public @ChildType int getChildType() {
return ChildType.SYNC_PROMO;
}
@Override
View getChildView(
int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView =
LegacySyncPromoView.create(
parent,
mRecentTabsManager.getProfile(),
SigninAccessPoint.RECENT_TABS);
}
return convertView;
}
}
/** A group containing the empty state illustration. */
// TODO(crbug.com/40923516): Consider using this PromoGroup subclass for the empty state
// implementation of LegacySyncPromoView.
class EmptyStatePromoGroup extends PromoGroup {
@Override
int getChildType() {
return ChildType.NONE;
}
@Override
View getChildView(
int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
if (convertView == null) {
LegacySyncPromoView legacySyncPromoView =
(LegacySyncPromoView)
LayoutInflater.from(parent.getContext())
.inflate(R.layout.legacy_sync_promo_view, parent, false);
legacySyncPromoView.setInitializeNotRequired();
legacySyncPromoView
.getEmptyStateTitle()
.setText(R.string.recent_tabs_no_tabs_empty_state);
legacySyncPromoView
.getEmptyStateDescription()
.setText(R.string.recent_tabs_sign_in_on_other_devices);
int emptyViewImageResId =
DeviceFormFactor.isNonMultiDisplayContextOnTablet(parent.getContext())
? R.drawable.tablet_recent_tab_empty_state_illustration
: R.drawable.phone_recent_tab_empty_state_illustration;
legacySyncPromoView.getEmptyStateImage().setImageResource(emptyViewImageResId);
legacySyncPromoView.getOldEmptyCardView().setVisibility(View.GONE);
legacySyncPromoView.getEmptyStateView().setVisibility(View.VISIBLE);
convertView = legacySyncPromoView;
}
return convertView;
}
}
/**
* A group containing tabs that were recently closed on this device and a link to the history
* page.
*/
class RecentlyClosedTabsGroup extends Group {
static final int ID_OPEN_IN_NEW_TAB = 1;
static final int ID_REMOVE_ALL = 2;
@Override
public @GroupType int getGroupType() {
return GroupType.CONTENT;
}
@Override
public int getChildrenCount() {
// The number of children is the number of recently closed tabs, plus one for the "Show
// full history" item.
return 1 + mRecentTabsManager.getRecentlyClosedEntries().size();
}
@Override
public @ChildType int getChildType() {
return ChildType.DEFAULT_CONTENT;
}
/**
* @param childPosition The index of an item in the recently closed list.
* @return Whether the item at childPosition is the link to the history page.
*/
private boolean isHistoryLink(int childPosition) {
return childPosition == mRecentTabsManager.getRecentlyClosedEntries().size();
}
@Override
public RecentlyClosedEntry getChild(int childPosition) {
if (isHistoryLink(childPosition)) return null;
return mRecentTabsManager.getRecentlyClosedEntries().get(childPosition);
}
@Override
public void configureChildView(int childPosition, ViewHolder viewHolder) {
// Reset the domain view text manually since it does not always reset itself, which can
// lead to wrong pairings of domain & title texts.
viewHolder.domainView.setText("");
viewHolder.domainView.setVisibility(View.GONE);
// Reset content description.
viewHolder.textView.setContentDescription(null);
if (isHistoryLink(childPosition)) {
viewHolder.textView.setText(R.string.show_full_history);
Bitmap historyIcon =
BitmapFactory.decodeResource(
mActivity.getResources(), R.drawable.ic_watch_later_24dp);
int size =
mActivity
.getResources()
.getDimensionPixelSize(R.dimen.tile_view_icon_size_modern);
Drawable drawable =
FaviconUtils.createRoundedBitmapDrawable(
mActivity.getResources(),
Bitmap.createScaledBitmap(historyIcon, size, size, true));
drawable.setColorFilter(
SemanticColorUtils.getDefaultIconColor(mActivity), PorterDuff.Mode.SRC_IN);
viewHolder.imageView.setImageDrawable(drawable);
viewHolder.itemLayout.setMinimumHeight(
mActivity
.getResources()
.getDimensionPixelSize(R.dimen.recent_tabs_show_history_item_size));
return;
}
viewHolder.itemLayout.setMinimumHeight(
mActivity
.getResources()
.getDimensionPixelSize(
R.dimen.recent_tabs_foreign_session_group_item_height));
RecentlyClosedEntry entry = getChild(childPosition);
if (!(entry instanceof RecentlyClosedTab)) {
int tabCount = 0;
if (entry instanceof RecentlyClosedGroup) {
RecentlyClosedGroup recentlyClosedGroup = (RecentlyClosedGroup) entry;
tabCount = recentlyClosedGroup.getTabs().size();
String groupTitle = recentlyClosedGroup.getTitle();
if (TextUtils.isEmpty(groupTitle)) {
viewHolder.textView.setText(
mActivity
.getResources()
.getString(
R.string.recent_tabs_group_closure_without_title,
tabCount));
String contentDescription =
mActivity
.getResources()
.getString(
R.string
.recent_tabs_group_closure_without_title_accessibility,
tabCount);
viewHolder.textView.setContentDescription(contentDescription);
} else {
viewHolder.textView.setText(
mActivity
.getResources()
.getString(
R.string.recent_tabs_group_closure_with_title,
groupTitle));
viewHolder.textView.setContentDescription(
mActivity
.getResources()
.getString(
R.string
.recent_tabs_group_closure_with_title_accessibility,
groupTitle));
}
}
if (entry instanceof RecentlyClosedBulkEvent) {
RecentlyClosedBulkEvent recentlyClosedBulkEvent =
(RecentlyClosedBulkEvent) entry;
tabCount = recentlyClosedBulkEvent.getTabs().size();
viewHolder.textView.setText(
mActivity
.getResources()
.getString(R.string.recent_tabs_bulk_closure, tabCount));
viewHolder.textView.setContentDescription(
mActivity
.getResources()
.getString(
R.string.recent_tabs_bulk_closure_accessibility,
tabCount));
}
// Entries without dates have a time of 0. TabRestoreService may not save timestamps
// between restarts.
if (entry.getDate().getTime() != 0L) {
String dateString =
DateFormat.getDateInstance(DateFormat.LONG, getPreferredLocale())
.format(entry.getDate());
viewHolder.domainView.setText(dateString);
viewHolder.domainView.setVisibility(View.VISIBLE);
}
loadTabCount(viewHolder, tabCount);
} else {
RecentlyClosedTab tab = (RecentlyClosedTab) entry;
String title = TitleUtil.getTitleForDisplay(tab.getTitle(), tab.getUrl());
viewHolder.textView.setText(title);
String domain = UrlUtilities.getDomainAndRegistry(tab.getUrl().getSpec(), false);
if (!TextUtils.isEmpty(domain)) {
viewHolder.domainView.setText(domain);
viewHolder.domainView.setVisibility(View.VISIBLE);
}
loadFavicon(viewHolder, tab.getUrl(), FaviconLocality.LOCAL);
}
}
@Override
public void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded) {
groupView.configureForRecentlyClosedTabs(isExpanded);
}
@Override
public void setCollapsed(boolean isCollapsed) {
mRecentTabsManager.setRecentlyClosedTabsCollapsed(isCollapsed);
}
@Override
public boolean isCollapsed() {
return mRecentTabsManager.isRecentlyClosedTabsCollapsed();
}
@Override
public boolean onChildClick(int childPosition) {
if (isHistoryLink(childPosition)) {
mRecentTabsManager.openHistoryPage();
return true;
}
RecentlyClosedEntry entry = getChild(childPosition);
if (entry instanceof RecentlyClosedTab) {
mRecentTabsManager.openRecentlyClosedTab(
(RecentlyClosedTab) entry, WindowOpenDisposition.CURRENT_TAB);
return true;
}
mRecentTabsManager.openRecentlyClosedEntry(entry);
return true;
}
@Override
public void onCreateContextMenuForGroup(ContextMenu menu, Activity activity) {}
@Override
public void onCreateContextMenuForChild(
final int childPosition, ContextMenu menu, Activity activity) {
final RecentlyClosedEntry recentlyClosedEntry = getChild(childPosition);
if (recentlyClosedEntry == null) return;
OnMenuItemClickListener listener =
item -> {
switch (item.getItemId()) {
case ID_REMOVE_ALL:
mRecentTabsManager.clearRecentlyClosedEntries();
break;
case ID_OPEN_IN_NEW_TAB:
mRecentTabsManager.openRecentlyClosedTab(
(RecentlyClosedTab) recentlyClosedEntry,
WindowOpenDisposition.NEW_BACKGROUND_TAB);
break;
default:
assert false;
}
return true;
};
if (recentlyClosedEntry instanceof RecentlyClosedTab) {
menu.add(
ContextMenu.NONE,
ID_OPEN_IN_NEW_TAB,
ContextMenu.NONE,
R.string.contextmenu_open_in_new_tab)
.setOnMenuItemClickListener(listener);
}
menu.add(ContextMenu.NONE, ID_REMOVE_ALL, ContextMenu.NONE, R.string.remove_all)
.setOnMenuItemClickListener(listener);
}
}
/** A group containing a blank separator. */
class SeparatorGroup extends Group {
private final boolean mIsVisible;
public SeparatorGroup(boolean isVisible) {
mIsVisible = isVisible;
}
@Override
public @GroupType int getGroupType() {
return mIsVisible ? GroupType.VISIBLE_SEPARATOR : GroupType.INVISIBLE_SEPARATOR;
}
@Override
public @ChildType int getChildType() {
return ChildType.NONE;
}
@Override
public int getChildrenCount() {
return 0;
}
@Override
public View getGroupView(boolean isExpanded, View convertView, ViewGroup parent) {
if (convertView == null) {
int layout =
mIsVisible
? R.layout.recent_tabs_group_separator_visible
: R.layout.recent_tabs_group_separator_invisible;
convertView = LayoutInflater.from(mActivity).inflate(layout, parent, false);
}
return convertView;
}
@Override
public void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded) {}
@Override
public void setCollapsed(boolean isCollapsed) {}
@Override
public boolean isCollapsed() {
return false;
}
}
private static class FaviconCache {
private final LruCache<GURL, Drawable> mMemoryCache;
public FaviconCache(int size) {
mMemoryCache = new LruCache<>(size);
}
Drawable getFaviconImage(GURL url) {
return mMemoryCache.get(url);
}
public void putFaviconImage(GURL url, Drawable image) {
mMemoryCache.put(url, image);
}
}
/**
* Creates a RecentTabsRowAdapter used to populate an ExpandableList with other
* devices and foreign tab cells.
*
* @param activity The Android activity this adapter will work in.
* @param recentTabsManager The RecentTabsManager that will act as the data source.
*/
public RecentTabsRowAdapter(Activity activity, RecentTabsManager recentTabsManager) {
mActivity = activity;
mRecentTabsManager = recentTabsManager;
mGroups = new ArrayList<>();
mFaviconCaches.put(FaviconLocality.LOCAL, new FaviconCache(MAX_NUM_FAVICONS_TO_CACHE));
mFaviconCaches.put(FaviconLocality.FOREIGN, new FaviconCache(MAX_NUM_FAVICONS_TO_CACHE));
Resources resources = activity.getResources();
mDefaultFaviconHelper = new DefaultFaviconHelper();
mFaviconSize = resources.getDimensionPixelSize(R.dimen.default_favicon_size);
mIconGenerator = FaviconUtils.createCircularIconGenerator(activity);
RecordHistogram.recordEnumeratedHistogram(
"HistoryPage.OtherDevicesMenu",
OtherSessionsActions.MENU_INITIALIZED,
OtherSessionsActions.NUM_ENTRIES);
}
/**
* ViewHolder class optimizes looking up table row fields. findViewById is only called once
* per row view initialization, and the references are cached here. Also stores a reference to
* the favicon image callback; so that we can make sure we load the correct favicon.
*/
private static class ViewHolder {
public TextView textView;
public TextView domainView;
public ImageView imageView;
public View itemLayout;
public FaviconImageCallback imageCallback;
}
private void loadTabCount(final ViewHolder viewHolder, int tabCount) {
RecentTabCountDrawable image = new RecentTabCountDrawable(mActivity);
image.updateTabCount(tabCount);
viewHolder.imageView.setImageDrawable(image);
}
private void loadFavicon(
final ViewHolder viewHolder, final GURL url, @FaviconLocality int locality) {
Drawable image;
if (url == null) {
// URL is null for print jobs, for example.
image = mDefaultFaviconHelper.getDefaultFaviconDrawable(mActivity, url, true);
} else {
image = mFaviconCaches.get(locality).getFaviconImage(url);
if (image == null) {
FaviconImageCallback imageCallback =
new FaviconImageCallback() {
@Override
public void onFaviconAvailable(Bitmap bitmap, GURL iconUrl) {
if (this != viewHolder.imageCallback) return;
Drawable faviconDrawable =
FaviconUtils.getIconDrawableWithFilter(
bitmap,
url,
mIconGenerator,
mDefaultFaviconHelper,
mActivity,
mFaviconSize);
mFaviconCaches.get(locality).putFaviconImage(url, faviconDrawable);
viewHolder.imageView.setImageDrawable(faviconDrawable);
}
};
viewHolder.imageCallback = imageCallback;
switch (locality) {
case FaviconLocality.LOCAL:
mRecentTabsManager.getLocalFaviconForUrl(url, mFaviconSize, imageCallback);
break;
case FaviconLocality.FOREIGN:
mRecentTabsManager.getForeignFaviconForUrl(
url, mFaviconSize, imageCallback);
break;
}
image = mDefaultFaviconHelper.getDefaultFaviconDrawable(mActivity, url, true);
}
}
viewHolder.imageView.setImageDrawable(image);
}
@Override
public View getChildView(
int groupPosition,
int childPosition,
boolean isLastChild,
View convertView,
ViewGroup parent) {
return getGroup(groupPosition)
.getChildView(childPosition, isLastChild, convertView, parent);
}
// BaseExpandableListAdapter group related implementations
@Override
public int getGroupCount() {
return mGroups.size();
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public Group getGroup(int groupPosition) {
return mGroups.get(groupPosition);
}
@Override
public View getGroupView(
int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
return getGroup(groupPosition).getGroupView(isExpanded, convertView, parent);
}
// BaseExpandableListAdapter child related implementations
@Override
public int getChildrenCount(int groupPosition) {
return getGroup(groupPosition).getChildrenCount();
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public Object getChild(int groupPosition, int childPosition) {
return getGroup(groupPosition).getChild(childPosition);
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
// BaseExpandableListAdapter misc. implementation
@Override
public boolean hasStableIds() {
return false;
}
@Override
public int getGroupType(int groupPosition) {
return getGroup(groupPosition).getGroupType();
}
@Override
public int getGroupTypeCount() {
return GroupType.NUM_ENTRIES;
}
private void addGroup(Group group) {
if (!DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity)) {
mGroups.add(group);
} else {
if (mGroups.size() == 0) {
mGroups.add(mInvisibleSeparatorGroup);
}
mGroups.add(group);
mGroups.add(mInvisibleSeparatorGroup);
}
}
@Override
public void notifyDataSetChanged() {
mGroups.clear();
addGroup(mRecentlyClosedTabsGroup);
for (ForeignSession session : mRecentTabsManager.getForeignSessions()) {
if (!mHasForeignDataRecorded) {
RecordHistogram.recordEnumeratedHistogram(
"HistoryPage.OtherDevicesMenu",
OtherSessionsActions.HAS_FOREIGN_DATA,
OtherSessionsActions.NUM_ENTRIES);
mHasForeignDataRecorded = true;
}
addGroup(new ForeignSessionGroup(session));
}
switch (mRecentTabsManager.getPromoState()) {
case SyncPromoState.NO_PROMO:
boolean recentlyClosedGroupIsOnlyHeader =
mRecentlyClosedTabsGroup.getChildrenCount() == 1;
if (ChromeFeatureList.isEnabled(
ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
&& recentlyClosedGroupIsOnlyHeader) {
addGroup(new EmptyStatePromoGroup());
}
break;
case SyncPromoState.PROMO_FOR_SIGNED_OUT_STATE:
addGroup(new PersonalizedSyncPromoGroup(ChildType.PERSONALIZED_SIGNIN_PROMO));
break;
case SyncPromoState.PROMO_FOR_SIGNED_IN_STATE:
addGroup(new PersonalizedSyncPromoGroup(ChildType.PERSONALIZED_SYNC_PROMO));
break;
case SyncPromoState.PROMO_FOR_SYNC_TURNED_OFF_STATE:
addGroup(new SyncPromoGroup());
break;
default:
assert false : "Unexpected value for promo type!";
}
// Add separator line after the recently closed tabs group.
int recentlyClosedIndex = mGroups.indexOf(mRecentlyClosedTabsGroup);
if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity)) {
if (recentlyClosedIndex != mGroups.size() - 2) {
mGroups.set(recentlyClosedIndex + 1, mVisibleSeparatorGroup);
}
}
super.notifyDataSetChanged();
}
@Override
public int getChildType(int groupPosition, int childPosition) {
return mGroups.get(groupPosition).getChildType();
}
@Override
public int getChildTypeCount() {
return ChildType.NUM_ENTRIES;
}
/** Retrieves the user's preferred locale from the app's configurations. */
private Locale getPreferredLocale() {
return mActivity.getResources().getConfiguration().getLocales().get(0);
}
}