1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.ntp;
6 
7 import android.app.Activity;
8 import android.content.res.Resources;
9 import android.graphics.Bitmap;
10 import android.graphics.BitmapFactory;
11 import android.graphics.PorterDuff;
12 import android.graphics.drawable.Drawable;
13 import android.text.TextUtils;
14 import android.util.ArrayMap;
15 import android.util.LruCache;
16 import android.view.ContextMenu;
17 import android.view.LayoutInflater;
18 import android.view.MenuItem.OnMenuItemClickListener;
19 import android.view.View;
20 import android.view.ViewGroup;
21 import android.widget.BaseExpandableListAdapter;
22 import android.widget.ImageView;
23 import android.widget.TextView;
24 
25 import androidx.annotation.IntDef;
26 
27 import org.chromium.base.ApiCompatibilityUtils;
28 import org.chromium.base.metrics.RecordHistogram;
29 import org.chromium.chrome.R;
30 import org.chromium.chrome.browser.ntp.ForeignSessionHelper.ForeignSession;
31 import org.chromium.chrome.browser.ntp.ForeignSessionHelper.ForeignSessionTab;
32 import org.chromium.chrome.browser.ntp.ForeignSessionHelper.ForeignSessionWindow;
33 import org.chromium.chrome.browser.signin.SyncPromoView;
34 import org.chromium.chrome.browser.ui.favicon.FaviconHelper.DefaultFaviconHelper;
35 import org.chromium.chrome.browser.ui.favicon.FaviconHelper.FaviconImageCallback;
36 import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
37 import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
38 import org.chromium.components.embedder_support.util.UrlUtilities;
39 import org.chromium.components.signin.metrics.SigninAccessPoint;
40 import org.chromium.ui.base.DeviceFormFactor;
41 import org.chromium.ui.mojom.WindowOpenDisposition;
42 
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Map;
48 
49 /**
50  * Row adapter for presenting recently closed tabs, synced tabs from other devices, the sync or
51  * sign in promo, and currently open tabs (only in document mode) in a grouped list view.
52  */
53 public class RecentTabsRowAdapter extends BaseExpandableListAdapter {
54     private static final int MAX_NUM_FAVICONS_TO_CACHE = 128;
55 
56     @IntDef({ChildType.NONE, ChildType.DEFAULT_CONTENT, ChildType.PERSONALIZED_SIGNIN_PROMO,
57             ChildType.PERSONALIZED_SYNC_PROMO, ChildType.SYNC_PROMO})
58     @Retention(RetentionPolicy.SOURCE)
59     private @interface ChildType {
60         // Values should be enumerated from 0 and can't have gaps.
61         int NONE = 0;
62         int DEFAULT_CONTENT = 1;
63         int PERSONALIZED_SIGNIN_PROMO = 2;
64         int PERSONALIZED_SYNC_PROMO = 3;
65         int SYNC_PROMO = 4;
66         /**
67          * Number of entries.
68          */
69         int NUM_ENTRIES = 5;
70     }
71 
72     @IntDef({GroupType.CONTENT, GroupType.VISIBLE_SEPARATOR, GroupType.INVISIBLE_SEPARATOR})
73     @Retention(RetentionPolicy.SOURCE)
74     private @interface GroupType {
75         // Values should be enumerated from 0 and can't have gaps.
76         int CONTENT = 0;
77         int VISIBLE_SEPARATOR = 1;
78         int INVISIBLE_SEPARATOR = 2;
79         /**
80          * Number of entries.
81          */
82         int NUM_ENTRIES = 3;
83     }
84 
85     // Values from the OtherSessionsActions enum in histograms.xml; do not change these values or
86     // histograms will be broken.
87     @IntDef({OtherSessionsActions.MENU_INITIALIZED, OtherSessionsActions.LINK_CLICKED,
88             OtherSessionsActions.COLLAPSE_SESSION, OtherSessionsActions.EXPAND_SESSION,
89             OtherSessionsActions.OPEN_ALL, OtherSessionsActions.HAS_FOREIGN_DATA,
90             OtherSessionsActions.HIDE_FOR_NOW})
91     @Retention(RetentionPolicy.SOURCE)
92     private @interface OtherSessionsActions {
93         int MENU_INITIALIZED = 0;
94         int LINK_CLICKED = 2;
95         int COLLAPSE_SESSION = 6;
96         int EXPAND_SESSION = 7;
97         int OPEN_ALL = 8;
98         int HAS_FOREIGN_DATA = 9;
99         int HIDE_FOR_NOW = 10;
100 
101         int NUM_ENTRIES = 11;
102     }
103 
104     @IntDef({FaviconLocality.LOCAL, FaviconLocality.FOREIGN})
105     @Retention(RetentionPolicy.SOURCE)
106     private @interface FaviconLocality {
107         int LOCAL = 0;
108         int FOREIGN = 1;
109 
110         int NUM_ENTRIES = 2;
111     }
112 
113     private final Activity mActivity;
114     private final List<Group> mGroups;
115     private final DefaultFaviconHelper mDefaultFaviconHelper;
116     private final RecentTabsManager mRecentTabsManager;
117     private final RecentlyClosedTabsGroup mRecentlyClosedTabsGroup = new RecentlyClosedTabsGroup();
118     private final SeparatorGroup mVisibleSeparatorGroup = new SeparatorGroup(true);
119     private final SeparatorGroup mInvisibleSeparatorGroup = new SeparatorGroup(false);
120     private final Map<Integer, FaviconCache> mFaviconCaches =
121             new ArrayMap<>(FaviconLocality.NUM_ENTRIES);
122     private final int mFaviconSize;
123     private boolean mHasForeignDataRecorded;
124     private RoundedIconGenerator mIconGenerator;
125 
126     /**
127      * A generic group of objects to be shown in the RecentTabsRowAdapter, such as the list of
128      * recently closed tabs.
129      */
130     abstract class Group {
131         /**
132          * @return The type of group: GroupType.CONTENT or GroupType.SEPARATOR.
133          */
getGroupType()134         abstract @GroupType int getGroupType();
135 
136         /**
137          * @return The number of children in this group.
138          */
getChildrenCount()139         abstract int getChildrenCount();
140 
141         /**
142          * @return The child type.
143          */
getChildType()144         abstract @ChildType int getChildType();
145 
146         /**
147          * @param childPosition The position for which to return the child.
148          * @return The child at the position childPosition.
149          */
getChild(int childPosition)150         Object getChild(int childPosition) {
151             return null;
152         }
153 
154         /**
155          * Returns the view corresponding to the child view at a given position.
156          *
157          * @param childPosition The position of the child.
158          * @param isLastChild Whether this child is the last one.
159          * @param convertView The re-usable child view (may be null).
160          * @param parent The parent view group.
161          *
162          * @return The view corresponding to the child.
163          */
getChildView(int childPosition, boolean isLastChild, View convertView, ViewGroup parent)164         View getChildView(int childPosition, boolean isLastChild,
165                 View convertView, ViewGroup parent) {
166             View childView = convertView;
167             if (childView == null) {
168                 LayoutInflater inflater = LayoutInflater.from(mActivity);
169                 childView = inflater.inflate(R.layout.recent_tabs_list_item, parent, false);
170 
171                 ViewHolder viewHolder = new ViewHolder();
172                 viewHolder.textView = (TextView) childView.findViewById(R.id.title_row);
173                 viewHolder.domainView = (TextView) childView.findViewById(R.id.domain_row);
174                 viewHolder.imageView = (ImageView) childView.findViewById(R.id.recent_tabs_favicon);
175                 viewHolder.imageView.setBackgroundResource(R.drawable.list_item_icon_modern_bg);
176                 viewHolder.itemLayout = childView.findViewById(R.id.recent_tabs_list_item_layout);
177                 childView.setTag(viewHolder);
178             }
179 
180             ViewHolder viewHolder = (ViewHolder) childView.getTag();
181             configureChildView(childPosition, viewHolder);
182 
183             return childView;
184         }
185 
186         /**
187          * Configures a view inflated from recent_tabs_list_item.xml to display information about
188          * a child in this group.
189          *
190          * @param childPosition The position of the child within this group.
191          * @param viewHolder The ViewHolder with references to pieces of the view.
192          */
configureChildView(int childPosition, ViewHolder viewHolder)193         void configureChildView(int childPosition, ViewHolder viewHolder) {}
194 
195         /**
196          * Returns the view corresponding to this group.
197          *
198          * @param isExpanded Whether the group is expanded.
199          * @param convertView The re-usable group view (may be null).
200          * @param parent The parent view group.
201          *
202          * @return The view corresponding to the group.
203          */
getGroupView(boolean isExpanded, View convertView, ViewGroup parent)204         public View getGroupView(boolean isExpanded, View convertView, ViewGroup parent) {
205             RecentTabsGroupView groupView = (RecentTabsGroupView) convertView;
206             if (groupView == null) {
207                 groupView = (RecentTabsGroupView) LayoutInflater.from(mActivity).inflate(
208                         R.layout.recent_tabs_group_item, parent, false);
209             }
210             configureGroupView(groupView, isExpanded);
211             return groupView;
212         }
213 
214         /**
215          * Configures an RecentTabsGroupView to display the header of this group.
216          * @param groupView The RecentTabsGroupView to configure.
217          * @param isExpanded Whether the view is currently expanded.
218          */
configureGroupView(RecentTabsGroupView groupView, boolean isExpanded)219         abstract void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded);
220 
221         /**
222          * Sets whether this group is collapsed (i.e. whether only the header is visible).
223          */
setCollapsed(boolean isCollapsed)224         abstract void setCollapsed(boolean isCollapsed);
225 
226         /**
227          * @return Whether this group is collapsed.
228          */
isCollapsed()229         abstract boolean isCollapsed();
230 
231         /**
232          * Called when a child item is clicked.
233          * @param childPosition The position of the child in the group.
234          * @return Whether the click was handled.
235          */
onChildClick(int childPosition)236         boolean onChildClick(int childPosition) {
237             return false;
238         }
239 
240         /**
241          * Called when the context menu for the group view is being built.
242          * @param menu The context menu being built.
243          * @param activity The current activity.
244          */
onCreateContextMenuForGroup(ContextMenu menu, Activity activity)245         void onCreateContextMenuForGroup(ContextMenu menu, Activity activity) {
246         }
247 
248         /**
249          * Called when a context menu for one of the child views is being built.
250          * @param childPosition The position of the child in the group.
251          * @param menu The context menu being built.
252          * @param activity The current activity.
253          */
onCreateContextMenuForChild(int childPosition, ContextMenu menu, Activity activity)254         void onCreateContextMenuForChild(int childPosition, ContextMenu menu,
255                 Activity activity) {
256         }
257     }
258 
259     /**
260      * A group containing all the tabs associated with a foreign session from a synced device.
261      */
262     class ForeignSessionGroup extends Group {
263         private final ForeignSession mForeignSession;
264 
ForeignSessionGroup(ForeignSession foreignSession)265         ForeignSessionGroup(ForeignSession foreignSession) {
266             mForeignSession = foreignSession;
267         }
268 
269         @Override
getGroupType()270         public @GroupType int getGroupType() {
271             return GroupType.CONTENT;
272         }
273 
274         @Override
getChildrenCount()275         public int getChildrenCount() {
276             int count = 0;
277             for (ForeignSessionWindow window : mForeignSession.windows) {
278                 count += window.tabs.size();
279             }
280             return count;
281         }
282 
283         @Override
getChildType()284         public @ChildType int getChildType() {
285             return ChildType.DEFAULT_CONTENT;
286         }
287 
288         @Override
getChild(int childPosition)289         public ForeignSessionTab getChild(int childPosition) {
290             for (ForeignSessionWindow window : mForeignSession.windows) {
291                 if (childPosition < window.tabs.size()) {
292                     return window.tabs.get(childPosition);
293                 }
294                 childPosition -= window.tabs.size();
295             }
296             assert false;
297             return null;
298         }
299 
300         @Override
configureChildView(int childPosition, ViewHolder viewHolder)301         public void configureChildView(int childPosition, ViewHolder viewHolder) {
302             ForeignSessionTab sessionTab = getChild(childPosition);
303             String text = TextUtils.isEmpty(sessionTab.title) ? sessionTab.url : sessionTab.title;
304             viewHolder.textView.setText(text);
305             String domain = UrlUtilities.getDomainAndRegistry(sessionTab.url, false);
306             if (!TextUtils.isEmpty(domain)) {
307                 viewHolder.domainView.setText(domain);
308                 viewHolder.domainView.setVisibility(View.VISIBLE);
309             } else {
310                 viewHolder.domainView.setText("");
311                 viewHolder.domainView.setVisibility(View.GONE);
312             }
313             loadFavicon(viewHolder, sessionTab.url, FaviconLocality.FOREIGN);
314         }
315 
316         @Override
configureGroupView(RecentTabsGroupView groupView, boolean isExpanded)317         public void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded) {
318             groupView.configureForForeignSession(mForeignSession, isExpanded);
319         }
320 
321         @Override
setCollapsed(boolean isCollapsed)322         public void setCollapsed(boolean isCollapsed) {
323             if (isCollapsed) {
324                 RecordHistogram.recordEnumeratedHistogram("HistoryPage.OtherDevicesMenu",
325                         OtherSessionsActions.COLLAPSE_SESSION, OtherSessionsActions.NUM_ENTRIES);
326             } else {
327                 RecordHistogram.recordEnumeratedHistogram("HistoryPage.OtherDevicesMenu",
328                         OtherSessionsActions.EXPAND_SESSION, OtherSessionsActions.NUM_ENTRIES);
329             }
330             mRecentTabsManager.setForeignSessionCollapsed(mForeignSession, isCollapsed);
331         }
332 
333         @Override
isCollapsed()334         public boolean isCollapsed() {
335             return mRecentTabsManager.getForeignSessionCollapsed(mForeignSession);
336         }
337 
338         @Override
onChildClick(int childPosition)339         public boolean onChildClick(int childPosition) {
340             RecordHistogram.recordEnumeratedHistogram("HistoryPage.OtherDevicesMenu",
341                     OtherSessionsActions.LINK_CLICKED, OtherSessionsActions.NUM_ENTRIES);
342             ForeignSessionTab foreignSessionTab = getChild(childPosition);
343             mRecentTabsManager.openForeignSessionTab(mForeignSession, foreignSessionTab,
344                     WindowOpenDisposition.CURRENT_TAB);
345             return true;
346         }
347 
348         @Override
onCreateContextMenuForGroup(ContextMenu menu, Activity activity)349         public void onCreateContextMenuForGroup(ContextMenu menu, Activity activity) {
350             menu.add(R.string.recent_tabs_open_all_menu_option).setOnMenuItemClickListener(item -> {
351                 RecordHistogram.recordEnumeratedHistogram("HistoryPage.OtherDevicesMenu",
352                         OtherSessionsActions.OPEN_ALL, OtherSessionsActions.NUM_ENTRIES);
353                 openAllTabs();
354                 return true;
355             });
356             menu.add(R.string.recent_tabs_hide_menu_option).setOnMenuItemClickListener(item -> {
357                 RecordHistogram.recordEnumeratedHistogram("HistoryPage.OtherDevicesMenu",
358                         OtherSessionsActions.HIDE_FOR_NOW, OtherSessionsActions.NUM_ENTRIES);
359                 mRecentTabsManager.deleteForeignSession(mForeignSession);
360                 return true;
361             });
362         }
363 
364         @Override
onCreateContextMenuForChild(int childPosition, ContextMenu menu, Activity activity)365         public void onCreateContextMenuForChild(int childPosition, ContextMenu menu,
366                 Activity activity) {
367             final ForeignSessionTab foreignSessionTab = getChild(childPosition);
368             OnMenuItemClickListener listener = item -> {
369                 mRecentTabsManager.openForeignSessionTab(mForeignSession, foreignSessionTab,
370                         WindowOpenDisposition.NEW_BACKGROUND_TAB);
371                 return true;
372             };
373             menu.add(R.string.contextmenu_open_in_new_tab).setOnMenuItemClickListener(listener);
374         }
375 
openAllTabs()376         private void openAllTabs() {
377             ForeignSessionTab firstTab = null;
378             for (ForeignSessionWindow window : mForeignSession.windows) {
379                 for (ForeignSessionTab tab : window.tabs) {
380                     if (firstTab == null) {
381                         firstTab = tab;
382                     } else {
383                         mRecentTabsManager.openForeignSessionTab(
384                                 mForeignSession, tab, WindowOpenDisposition.NEW_BACKGROUND_TAB);
385                     }
386                 }
387             }
388             // Open the first tab last because calls to openForeignSessionTab after one for
389             // CURRENT_TAB are ignored.
390             if (firstTab != null) {
391                 mRecentTabsManager.openForeignSessionTab(
392                         mForeignSession, firstTab, WindowOpenDisposition.CURRENT_TAB);
393             }
394         }
395     }
396 
397     /**
398      * A base group for promos.
399      */
400     private abstract class PromoGroup extends Group {
401         @Override
402         @GroupType
getGroupType()403         int getGroupType() {
404             return GroupType.CONTENT;
405         }
406 
407         @Override
getChildrenCount()408         int getChildrenCount() {
409             return 1;
410         }
411 
412         @Override
configureGroupView(RecentTabsGroupView groupView, boolean isExpanded)413         void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded) {
414             groupView.configureForPromo(isExpanded);
415         }
416 
417         @Override
setCollapsed(boolean isCollapsed)418         void setCollapsed(boolean isCollapsed) {
419             mRecentTabsManager.setPromoCollapsed(isCollapsed);
420         }
421 
422         @Override
isCollapsed()423         boolean isCollapsed() {
424             return mRecentTabsManager.isPromoCollapsed();
425         }
426     }
427 
428     /**
429      * A group containing the personalized signin promo.
430      */
431     class PersonalizedSigninPromoGroup extends PromoGroup {
432         @Override
433         @ChildType
getChildType()434         int getChildType() {
435             return ChildType.PERSONALIZED_SIGNIN_PROMO;
436         }
437 
438         @Override
getChildView( int childPosition, boolean isLastChild, View convertView, ViewGroup parent)439         View getChildView(
440                 int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
441             if (convertView == null) {
442                 LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
443                 convertView = layoutInflater.inflate(
444                         R.layout.personalized_signin_promo_view_recent_tabs, parent, false);
445             }
446             mRecentTabsManager.setupPersonalizedSigninPromo(
447                     convertView.findViewById(R.id.signin_promo_view_container));
448             return convertView;
449         }
450     }
451 
452     /**
453      * A group containing the personalized sync promo.
454      */
455     class PersonalizedSyncPromoGroup extends PromoGroup {
456         @Override
457         @ChildType
getChildType()458         int getChildType() {
459             return ChildType.PERSONALIZED_SYNC_PROMO;
460         }
461 
462         @Override
getChildView( int childPosition, boolean isLastChild, View convertView, ViewGroup parent)463         View getChildView(
464                 int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
465             if (convertView == null) {
466                 LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
467                 convertView = layoutInflater.inflate(
468                         R.layout.personalized_signin_promo_view_recent_tabs, parent, false);
469             }
470             mRecentTabsManager.setupPersonalizedSyncPromo(
471                     convertView.findViewById(R.id.signin_promo_view_container));
472             return convertView;
473         }
474     }
475 
476     /**
477      * A group containing the sync promo.
478      */
479     class SyncPromoGroup extends PromoGroup {
480         @Override
getChildType()481         public @ChildType int getChildType() {
482             return ChildType.SYNC_PROMO;
483         }
484 
485         @Override
getChildView( int childPosition, boolean isLastChild, View convertView, ViewGroup parent)486         View getChildView(
487                 int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
488             if (convertView == null) {
489                 convertView = SyncPromoView.create(parent, SigninAccessPoint.RECENT_TABS);
490             }
491             return convertView;
492         }
493     }
494 
495     /**
496      * A group containing tabs that were recently closed on this device and a link to the history
497      * page.
498      */
499     class RecentlyClosedTabsGroup extends Group {
500         static final int ID_OPEN_IN_NEW_TAB = 1;
501         static final int ID_REMOVE_ALL = 2;
502 
503         @Override
getGroupType()504         public @GroupType int getGroupType() {
505             return GroupType.CONTENT;
506         }
507 
508         @Override
getChildrenCount()509         public int getChildrenCount() {
510             // The number of children is the number of recently closed tabs, plus one for the "Show
511             // full history" item.
512             return 1 + mRecentTabsManager.getRecentlyClosedTabs().size();
513         }
514 
515         @Override
getChildType()516         public @ChildType int getChildType() {
517             return ChildType.DEFAULT_CONTENT;
518         }
519 
520         /**
521          * @param childPosition The index of an item in the recently closed list.
522          * @return Whether the item at childPosition is the link to the history page.
523          */
isHistoryLink(int childPosition)524         private boolean isHistoryLink(int childPosition) {
525             return childPosition == mRecentTabsManager.getRecentlyClosedTabs().size();
526         }
527 
528         @Override
getChild(int childPosition)529         public RecentlyClosedTab getChild(int childPosition) {
530             if (isHistoryLink(childPosition)) return null;
531             return mRecentTabsManager.getRecentlyClosedTabs().get(childPosition);
532         }
533 
534         @Override
configureChildView(int childPosition, ViewHolder viewHolder)535         public void configureChildView(int childPosition, ViewHolder viewHolder) {
536             // Reset the domain view text manually since it does not always reset itself, which can
537             // lead to wrong pairings of domain & title texts.
538             viewHolder.domainView.setText("");
539             viewHolder.domainView.setVisibility(View.GONE);
540             if (isHistoryLink(childPosition)) {
541                 viewHolder.textView.setText(R.string.show_full_history);
542                 Bitmap historyIcon = BitmapFactory.decodeResource(
543                         mActivity.getResources(), R.drawable.ic_watch_later_24dp);
544                 int size = mActivity.getResources().getDimensionPixelSize(
545                         R.dimen.tile_view_icon_size_modern);
546                 Drawable drawable =
547                         FaviconUtils.createRoundedBitmapDrawable(mActivity.getResources(),
548                                 Bitmap.createScaledBitmap(historyIcon, size, size, true));
549                 drawable.setColorFilter(ApiCompatibilityUtils.getColor(mActivity.getResources(),
550                                                 R.color.default_icon_color),
551                         PorterDuff.Mode.SRC_IN);
552                 viewHolder.imageView.setImageDrawable(drawable);
553                 viewHolder.itemLayout.getLayoutParams().height =
554                         mActivity.getResources().getDimensionPixelSize(
555                                 R.dimen.recent_tabs_show_history_item_size);
556                 return;
557             }
558             viewHolder.itemLayout.getLayoutParams().height =
559                     mActivity.getResources().getDimensionPixelSize(
560                             R.dimen.recent_tabs_foreign_session_group_item_height);
561             RecentlyClosedTab tab = getChild(childPosition);
562             String title = TitleUtil.getTitleForDisplay(tab.title, tab.url);
563             viewHolder.textView.setText(title);
564 
565             String domain = UrlUtilities.getDomainAndRegistry(tab.url.getSpec(), false);
566             if (!TextUtils.isEmpty(domain)) {
567                 viewHolder.domainView.setText(domain);
568                 viewHolder.domainView.setVisibility(View.VISIBLE);
569             }
570             loadFavicon(viewHolder, tab.url.getSpec(), FaviconLocality.LOCAL);
571         }
572 
573         @Override
configureGroupView(RecentTabsGroupView groupView, boolean isExpanded)574         public void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded) {
575             groupView.configureForRecentlyClosedTabs(isExpanded);
576         }
577 
578         @Override
setCollapsed(boolean isCollapsed)579         public void setCollapsed(boolean isCollapsed) {
580             mRecentTabsManager.setRecentlyClosedTabsCollapsed(isCollapsed);
581         }
582 
583         @Override
isCollapsed()584         public boolean isCollapsed() {
585             return mRecentTabsManager.isRecentlyClosedTabsCollapsed();
586         }
587 
588         @Override
onChildClick(int childPosition)589         public boolean onChildClick(int childPosition) {
590             if (isHistoryLink(childPosition)) {
591                 mRecentTabsManager.openHistoryPage();
592             } else {
593                 mRecentTabsManager.openRecentlyClosedTab(getChild(childPosition),
594                         WindowOpenDisposition.CURRENT_TAB);
595             }
596             return true;
597         }
598 
599         @Override
onCreateContextMenuForGroup(ContextMenu menu, Activity activity)600         public void onCreateContextMenuForGroup(ContextMenu menu, Activity activity) {
601         }
602 
603         @Override
onCreateContextMenuForChild(final int childPosition, ContextMenu menu, Activity activity)604         public void onCreateContextMenuForChild(final int childPosition, ContextMenu menu,
605                 Activity activity) {
606             final RecentlyClosedTab recentlyClosedTab = getChild(childPosition);
607             if (recentlyClosedTab == null) return;
608             OnMenuItemClickListener listener = item -> {
609                 switch (item.getItemId()) {
610                     case ID_REMOVE_ALL:
611                         mRecentTabsManager.clearRecentlyClosedTabs();
612                         break;
613                     case ID_OPEN_IN_NEW_TAB:
614                         mRecentTabsManager.openRecentlyClosedTab(
615                                 recentlyClosedTab, WindowOpenDisposition.NEW_BACKGROUND_TAB);
616                         break;
617                     default:
618                         assert false;
619                 }
620                 return true;
621             };
622             menu.add(ContextMenu.NONE, ID_OPEN_IN_NEW_TAB, ContextMenu.NONE,
623                     R.string.contextmenu_open_in_new_tab).setOnMenuItemClickListener(listener);
624             menu.add(ContextMenu.NONE, ID_REMOVE_ALL, ContextMenu.NONE,
625                     R.string.remove_all).setOnMenuItemClickListener(listener);
626         }
627     }
628 
629     /**
630      * A group containing a blank separator.
631      */
632     class SeparatorGroup extends Group {
633         private final boolean mIsVisible;
634 
SeparatorGroup(boolean isVisible)635         public SeparatorGroup(boolean isVisible) {
636             mIsVisible = isVisible;
637         }
638 
639         @Override
getGroupType()640         public @GroupType int getGroupType() {
641             return mIsVisible ? GroupType.VISIBLE_SEPARATOR : GroupType.INVISIBLE_SEPARATOR;
642         }
643 
644         @Override
getChildType()645         public @ChildType int getChildType() {
646             return ChildType.NONE;
647         }
648 
649         @Override
getChildrenCount()650         public int getChildrenCount() {
651             return 0;
652         }
653 
654         @Override
getGroupView(boolean isExpanded, View convertView, ViewGroup parent)655         public View getGroupView(boolean isExpanded, View convertView, ViewGroup parent) {
656             if (convertView == null) {
657                 int layout = mIsVisible
658                         ? R.layout.recent_tabs_group_separator_visible
659                         : R.layout.recent_tabs_group_separator_invisible;
660                 convertView = LayoutInflater.from(mActivity).inflate(layout, parent, false);
661             }
662             return convertView;
663         }
664 
665         @Override
configureGroupView(RecentTabsGroupView groupView, boolean isExpanded)666         public void configureGroupView(RecentTabsGroupView groupView, boolean isExpanded) {
667         }
668 
669         @Override
setCollapsed(boolean isCollapsed)670         public void setCollapsed(boolean isCollapsed) {
671         }
672 
673         @Override
isCollapsed()674         public boolean isCollapsed() {
675             return false;
676         }
677     }
678 
679     private static class FaviconCache {
680         private final LruCache<String, Drawable> mMemoryCache;
681 
FaviconCache(int size)682         public FaviconCache(int size) {
683             mMemoryCache = new LruCache<>(size);
684         }
685 
getFaviconImage(String url)686         Drawable getFaviconImage(String url) {
687             return mMemoryCache.get(url);
688         }
689 
putFaviconImage(String url, Drawable image)690         public void putFaviconImage(String url, Drawable image) {
691             mMemoryCache.put(url, image);
692         }
693     }
694 
695     /**
696      * Creates a RecentTabsRowAdapter used to populate an ExpandableList with other
697      * devices and foreign tab cells.
698      *
699      * @param activity The Android activity this adapter will work in.
700      * @param recentTabsManager The RecentTabsManager that will act as the data source.
701      */
RecentTabsRowAdapter(Activity activity, RecentTabsManager recentTabsManager)702     public RecentTabsRowAdapter(Activity activity, RecentTabsManager recentTabsManager) {
703         mActivity = activity;
704         mRecentTabsManager = recentTabsManager;
705         mGroups = new ArrayList<>();
706         mFaviconCaches.put(FaviconLocality.LOCAL, new FaviconCache(MAX_NUM_FAVICONS_TO_CACHE));
707         mFaviconCaches.put(FaviconLocality.FOREIGN, new FaviconCache(MAX_NUM_FAVICONS_TO_CACHE));
708 
709         Resources resources = activity.getResources();
710         mDefaultFaviconHelper = new DefaultFaviconHelper();
711         mFaviconSize = resources.getDimensionPixelSize(R.dimen.default_favicon_size);
712 
713         mIconGenerator = FaviconUtils.createCircularIconGenerator(activity.getResources());
714 
715         RecordHistogram.recordEnumeratedHistogram("HistoryPage.OtherDevicesMenu",
716                 OtherSessionsActions.MENU_INITIALIZED, OtherSessionsActions.NUM_ENTRIES);
717     }
718 
719     /**
720      * ViewHolder class optimizes looking up table row fields. findViewById is only called once
721      * per row view initialization, and the references are cached here. Also stores a reference to
722      * the favicon image callback; so that we can make sure we load the correct favicon.
723      */
724     private static class ViewHolder {
725         public TextView textView;
726         public TextView domainView;
727         public ImageView imageView;
728         public View itemLayout;
729         public FaviconImageCallback imageCallback;
730     }
731 
loadFavicon( final ViewHolder viewHolder, final String url, @FaviconLocality int locality)732     private void loadFavicon(
733             final ViewHolder viewHolder, final String url, @FaviconLocality int locality) {
734         Drawable image;
735         if (url == null) {
736             // URL is null for print jobs, for example.
737             image = mDefaultFaviconHelper.getDefaultFaviconDrawable(
738                     mActivity.getResources(), url, true);
739         } else {
740             image = mFaviconCaches.get(locality).getFaviconImage(url);
741             if (image == null) {
742                 FaviconImageCallback imageCallback = new FaviconImageCallback() {
743                     @Override
744                     public void onFaviconAvailable(Bitmap bitmap, String iconUrl) {
745                         if (this != viewHolder.imageCallback) return;
746                         Drawable faviconDrawable = FaviconUtils.getIconDrawableWithFilter(bitmap,
747                                 url, mIconGenerator, mDefaultFaviconHelper,
748                                 mActivity.getResources(), mFaviconSize);
749                         mFaviconCaches.get(locality).putFaviconImage(url, faviconDrawable);
750                         viewHolder.imageView.setImageDrawable(faviconDrawable);
751                     }
752                 };
753                 viewHolder.imageCallback = imageCallback;
754                 switch (locality) {
755                     case FaviconLocality.LOCAL:
756                         mRecentTabsManager.getLocalFaviconForUrl(url, mFaviconSize, imageCallback);
757                         break;
758                     case FaviconLocality.FOREIGN:
759                         mRecentTabsManager.getForeignFaviconForUrl(
760                                 url, mFaviconSize, imageCallback);
761                         break;
762                 }
763 
764                 image = mDefaultFaviconHelper.getDefaultFaviconDrawable(
765                         mActivity.getResources(), url, true);
766             }
767         }
768         viewHolder.imageView.setImageDrawable(image);
769     }
770 
771     @Override
getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent)772     public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
773             View convertView, ViewGroup parent) {
774         return getGroup(groupPosition)
775                 .getChildView(childPosition, isLastChild, convertView, parent);
776     }
777 
778     // BaseExpandableListAdapter group related implementations
779     @Override
getGroupCount()780     public int getGroupCount() {
781         return mGroups.size();
782     }
783 
784     @Override
getGroupId(int groupPosition)785     public long getGroupId(int groupPosition) {
786         return groupPosition;
787     }
788 
789     @Override
getGroup(int groupPosition)790     public Group getGroup(int groupPosition) {
791         return mGroups.get(groupPosition);
792     }
793 
794     @Override
getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent)795     public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
796             ViewGroup parent) {
797         return getGroup(groupPosition).getGroupView(isExpanded, convertView, parent);
798     }
799 
800     // BaseExpandableListAdapter child related implementations
801     @Override
getChildrenCount(int groupPosition)802     public int getChildrenCount(int groupPosition) {
803         return getGroup(groupPosition).getChildrenCount();
804     }
805 
806     @Override
getChildId(int groupPosition, int childPosition)807     public long getChildId(int groupPosition, int childPosition) {
808         return childPosition;
809     }
810 
811     @Override
getChild(int groupPosition, int childPosition)812     public Object getChild(int groupPosition, int childPosition) {
813         return getGroup(groupPosition).getChild(childPosition);
814     }
815 
816     @Override
isChildSelectable(int groupPosition, int childPosition)817     public boolean isChildSelectable(int groupPosition, int childPosition) {
818         return true;
819     }
820 
821     // BaseExpandableListAdapter misc. implementation
822     @Override
hasStableIds()823     public boolean hasStableIds() {
824         return false;
825     }
826 
827     @Override
getGroupType(int groupPosition)828     public int getGroupType(int groupPosition) {
829         return getGroup(groupPosition).getGroupType();
830     }
831 
832     @Override
getGroupTypeCount()833     public int getGroupTypeCount() {
834         return GroupType.NUM_ENTRIES;
835     }
836 
addGroup(Group group)837     private void addGroup(Group group) {
838         if (!DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity)) {
839             mGroups.add(group);
840         } else {
841             if (mGroups.size() == 0) {
842                 mGroups.add(mInvisibleSeparatorGroup);
843             }
844             mGroups.add(group);
845             mGroups.add(mInvisibleSeparatorGroup);
846         }
847     }
848 
849     @Override
notifyDataSetChanged()850     public void notifyDataSetChanged() {
851         mGroups.clear();
852         addGroup(mRecentlyClosedTabsGroup);
853         for (ForeignSession session : mRecentTabsManager.getForeignSessions()) {
854             if (!mHasForeignDataRecorded) {
855                 RecordHistogram.recordEnumeratedHistogram("HistoryPage.OtherDevicesMenu",
856                         OtherSessionsActions.HAS_FOREIGN_DATA, OtherSessionsActions.NUM_ENTRIES);
857                 mHasForeignDataRecorded = true;
858             }
859             addGroup(new ForeignSessionGroup(session));
860         }
861 
862         switch (mRecentTabsManager.getPromoType()) {
863             case RecentTabsManager.PromoState.PROMO_NONE:
864                 break;
865             case RecentTabsManager.PromoState.PROMO_SIGNIN_PERSONALIZED:
866                 addGroup(new PersonalizedSigninPromoGroup());
867                 break;
868             case RecentTabsManager.PromoState.PROMO_SYNC_PERSONALIZED:
869                 addGroup(new PersonalizedSyncPromoGroup());
870                 break;
871             case RecentTabsManager.PromoState.PROMO_SYNC:
872                 addGroup(new SyncPromoGroup());
873                 break;
874             default:
875                 assert false : "Unexpected value for promo type!";
876         }
877 
878         // Add separator line after the recently closed tabs group.
879         int recentlyClosedIndex = mGroups.indexOf(mRecentlyClosedTabsGroup);
880         if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity)) {
881             if (recentlyClosedIndex != mGroups.size() - 2) {
882                 mGroups.set(recentlyClosedIndex + 1, mVisibleSeparatorGroup);
883             }
884         }
885 
886         super.notifyDataSetChanged();
887     }
888 
889     @Override
getChildType(int groupPosition, int childPosition)890     public int getChildType(int groupPosition, int childPosition) {
891         return mGroups.get(groupPosition).getChildType();
892     }
893 
894     @Override
getChildTypeCount()895     public int getChildTypeCount() {
896         return ChildType.NUM_ENTRIES;
897     }
898 }
899