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