1 // Copyright 2017 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.suggestions.tile; 6 7 import android.graphics.Bitmap; 8 import android.util.SparseArray; 9 import android.view.ContextMenu; 10 import android.view.ContextMenu.ContextMenuInfo; 11 import android.view.View; 12 import android.view.View.OnClickListener; 13 import android.view.View.OnCreateContextMenuListener; 14 15 import androidx.annotation.IntDef; 16 import androidx.annotation.Nullable; 17 import androidx.annotation.VisibleForTesting; 18 19 import org.chromium.base.Callback; 20 import org.chromium.chrome.browser.explore_sites.ExploreSitesBridge; 21 import org.chromium.chrome.browser.explore_sites.ExploreSitesCatalogUpdateRequestSource; 22 import org.chromium.chrome.browser.native_page.ContextMenuManager; 23 import org.chromium.chrome.browser.native_page.ContextMenuManager.ContextMenuItemId; 24 import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; 25 import org.chromium.chrome.browser.offlinepages.OfflinePageItem; 26 import org.chromium.chrome.browser.profiles.Profile; 27 import org.chromium.chrome.browser.suggestions.SiteSuggestion; 28 import org.chromium.chrome.browser.suggestions.SuggestionsConfig; 29 import org.chromium.chrome.browser.suggestions.SuggestionsMetrics; 30 import org.chromium.chrome.browser.suggestions.SuggestionsOfflineModelObserver; 31 import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegate; 32 import org.chromium.chrome.browser.suggestions.mostvisited.MostVisitedSites; 33 import org.chromium.components.favicon.IconType; 34 import org.chromium.components.favicon.LargeIconBridge; 35 import org.chromium.ui.mojom.WindowOpenDisposition; 36 import org.chromium.url.GURL; 37 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.ArrayList; 41 import java.util.Collection; 42 import java.util.List; 43 44 /** 45 * The model and controller for a group of site suggestion tiles. 46 */ 47 public class TileGroup implements MostVisitedSites.Observer { 48 /** 49 * Performs work in other parts of the system that the {@link TileGroup} should not know about. 50 */ 51 public interface Delegate { 52 /** 53 * @param tile The tile corresponding to the most visited item to remove. 54 * @param removalUndoneCallback The callback to invoke if the removal is reverted. The 55 * callback's argument is the URL being restored. 56 */ removeMostVisitedItem(Tile tile, Callback<GURL> removalUndoneCallback)57 void removeMostVisitedItem(Tile tile, Callback<GURL> removalUndoneCallback); 58 openMostVisitedItem(int windowDisposition, Tile tile)59 void openMostVisitedItem(int windowDisposition, Tile tile); 60 61 /** 62 * Gets the list of most visited sites. 63 * @param observer The observer to be notified with the list of sites. 64 * @param maxResults The maximum number of sites to retrieve. 65 */ setMostVisitedSitesObserver(MostVisitedSites.Observer observer, int maxResults)66 void setMostVisitedSitesObserver(MostVisitedSites.Observer observer, int maxResults); 67 68 /** 69 * Called when the tile group has completely finished loading (all views will be inflated 70 * and any dependent resources will have been loaded). 71 * @param tiles The tiles owned by the {@link TileGroup}. Used to record metrics. 72 */ onLoadingComplete(List<Tile> tiles)73 void onLoadingComplete(List<Tile> tiles); 74 75 /** 76 * To be called before this instance is abandoned to the garbage collector so it can do any 77 * necessary cleanups. This instance must not be used after this method is called. 78 */ destroy()79 void destroy(); 80 } 81 82 /** 83 * An observer for events in the {@link TileGroup}. 84 */ 85 public interface Observer { 86 /** 87 * Called when the tile group is initialised and when any of the tile data has changed, 88 * such as an icon, url, or title. 89 */ onTileDataChanged()90 void onTileDataChanged(); 91 92 /** 93 * Called when the number of tiles has changed. 94 */ onTileCountChanged()95 void onTileCountChanged(); 96 97 /** 98 * Called when a tile icon has changed. 99 * @param tile The tile for which the icon has changed. 100 */ onTileIconChanged(Tile tile)101 void onTileIconChanged(Tile tile); 102 103 /** 104 * Called when the visibility of a tile's offline badge has changed. 105 * @param tile The tile for which the visibility of the offline badge has changed. 106 */ onTileOfflineBadgeVisibilityChanged(Tile tile)107 void onTileOfflineBadgeVisibilityChanged(Tile tile); 108 } 109 110 /** 111 * A delegate to allow {@link TileRenderer} to setup behaviours for the newly created views 112 * associated to a Tile. 113 */ 114 public interface TileSetupDelegate { 115 /** 116 * Returns a delegate that will handle user interactions with the view created for the tile. 117 */ createInteractionDelegate(Tile tile)118 TileInteractionDelegate createInteractionDelegate(Tile tile); 119 120 /** 121 * Returns a callback to be invoked when the icon for the provided tile is loaded. It will 122 * be responsible for updating the tile data and triggering the visual refresh. 123 */ createIconLoadCallback(Tile tile)124 LargeIconBridge.LargeIconCallback createIconLoadCallback(Tile tile); 125 } 126 127 /** 128 * Delegate for handling interactions with tiles. 129 */ 130 public interface TileInteractionDelegate extends OnClickListener, OnCreateContextMenuListener { 131 /** 132 * Set a runnable for click events on the tile. This is primarily used to track interaction 133 * with the tile used by feature engagement purposes. 134 * @param clickRunnable The {@link Runnable} to be executed when tile is clicked. 135 */ setOnClickRunnable(Runnable clickRunnable)136 void setOnClickRunnable(Runnable clickRunnable); 137 } 138 139 /** 140 * Constants used to track the current operations on the group and notify the {@link Delegate} 141 * when the expected sequence of potentially asynchronous operations is complete. 142 */ 143 @VisibleForTesting 144 @IntDef({TileTask.FETCH_DATA, TileTask.SCHEDULE_ICON_FETCH, TileTask.FETCH_ICON}) 145 @Retention(RetentionPolicy.SOURCE) 146 @interface TileTask { 147 /** 148 * An event that should result in new data being loaded happened. 149 * Can be an asynchronous task, spanning from when the {@link Observer} is registered to 150 * when the initial load completes. 151 */ 152 int FETCH_DATA = 1; 153 154 /** 155 * New tile data has been loaded and we are expecting the related icons to be fetched. 156 * Can be an asynchronous task, as we rely on it being triggered by the embedder, some time 157 * after {@link Observer#onTileDataChanged()} is called. 158 */ 159 int SCHEDULE_ICON_FETCH = 2; 160 161 /** 162 * The icon for a tile is being fetched. 163 * Asynchronous task, that is started for each icon that needs to be loaded. 164 */ 165 int FETCH_ICON = 3; 166 } 167 168 private final SuggestionsUiDelegate mUiDelegate; 169 private final ContextMenuManager mContextMenuManager; 170 private final Delegate mTileGroupDelegate; 171 private final Observer mObserver; 172 private final TileRenderer mTileRenderer; 173 174 /** 175 * Tracks the tasks currently in flight. 176 * 177 * We only care about which ones are pending, not their order, and we can have multiple tasks 178 * pending of the same type. Hence exposing the type as Collection rather than List or Set. 179 */ 180 private final Collection<Integer> mPendingTasks = new ArrayList<>(); 181 182 /** Access point to offline related features. */ 183 private final OfflineModelObserver mOfflineModelObserver; 184 185 /** 186 * Source of truth for the tile data. Avoid keeping a reference to a tile in long running 187 * callbacks, as it might be thrown out before it is called. Use URL or site data to look it up 188 * at the right time instead. 189 * @see #findTile(SiteSuggestion) 190 * @see #findTilesForUrl(String) 191 */ 192 private SparseArray<List<Tile>> mTileSections = createEmptyTileData(); 193 194 /** Most recently received tile data that has not been displayed yet. */ 195 @Nullable 196 private List<SiteSuggestion> mPendingTiles; 197 198 /** 199 * URL of the most recently removed tile. Used to identify when a tile removal is confirmed by 200 * the tile backend. 201 */ 202 @Nullable 203 private GURL mPendingRemovalUrl; 204 205 /** 206 * URL of the most recently added tile. Used to identify when a given tile's insertion is 207 * confirmed by the tile backend. This is relevant when a previously existing tile is removed, 208 * then the user undoes the action and wants that tile back. 209 */ 210 @Nullable 211 private GURL mPendingInsertionUrl; 212 213 private boolean mHasReceivedData; 214 private boolean mExploreSitesLoaded; 215 216 // TODO(dgn): Attempt to avoid cycling dependencies with TileRenderer. Is there a better way? 217 private final TileSetupDelegate mTileSetupDelegate = new TileSetupDelegate() { 218 @Override 219 public TileInteractionDelegate createInteractionDelegate(Tile tile) { 220 return new TileInteractionDelegateImpl(tile.getData()); 221 } 222 223 @Override 224 public LargeIconBridge.LargeIconCallback createIconLoadCallback(Tile tile) { 225 // TODO(dgn): We could save on fetches by avoiding a new one when there is one pending 226 // for the same URL, and applying the result to all matched URLs. 227 boolean trackLoad = 228 isLoadTracked() && tile.getSectionType() == TileSectionType.PERSONALIZED; 229 if (trackLoad) addTask(TileTask.FETCH_ICON); 230 return new LargeIconCallbackImpl(tile.getData(), trackLoad); 231 } 232 }; 233 234 /** 235 * @param tileRenderer Used to render icons. 236 * @param uiDelegate Delegate used to interact with the rest of the system. 237 * @param contextMenuManager Used to handle context menu invocations on the tiles. 238 * @param tileGroupDelegate Used for interactions with the Most Visited backend. 239 * @param observer Will be notified of changes to the tile data. 240 * @param offlinePageBridge Used to update the offline badge of the tiles. 241 */ TileGroup(TileRenderer tileRenderer, SuggestionsUiDelegate uiDelegate, ContextMenuManager contextMenuManager, Delegate tileGroupDelegate, Observer observer, OfflinePageBridge offlinePageBridge)242 public TileGroup(TileRenderer tileRenderer, SuggestionsUiDelegate uiDelegate, 243 ContextMenuManager contextMenuManager, Delegate tileGroupDelegate, Observer observer, 244 OfflinePageBridge offlinePageBridge) { 245 mUiDelegate = uiDelegate; 246 mContextMenuManager = contextMenuManager; 247 mTileGroupDelegate = tileGroupDelegate; 248 mObserver = observer; 249 mTileRenderer = tileRenderer; 250 mOfflineModelObserver = new OfflineModelObserver(offlinePageBridge); 251 mUiDelegate.addDestructionObserver(mOfflineModelObserver); 252 } 253 254 @Override onSiteSuggestionsAvailable(List<SiteSuggestion> siteSuggestions)255 public void onSiteSuggestionsAvailable(List<SiteSuggestion> siteSuggestions) { 256 // Only transforms the incoming tiles and stores them in a buffer for when we decide to 257 // refresh the tiles in the UI. 258 259 boolean removalCompleted = mPendingRemovalUrl != null; 260 boolean insertionCompleted = mPendingInsertionUrl == null; 261 262 mPendingTiles = new ArrayList<>(); 263 for (SiteSuggestion suggestion : siteSuggestions) { 264 mPendingTiles.add(suggestion); 265 266 // Only tiles in the personal section can be modified. 267 if (suggestion.sectionType != TileSectionType.PERSONALIZED) continue; 268 if (suggestion.url.equals(mPendingRemovalUrl)) removalCompleted = false; 269 if (suggestion.url.equals(mPendingInsertionUrl)) insertionCompleted = true; 270 if (suggestion.source == TileSource.EXPLORE && !mExploreSitesLoaded) { 271 mExploreSitesLoaded = true; 272 ExploreSitesBridge.initializeCatalog(Profile.getLastUsedRegularProfile(), 273 ExploreSitesCatalogUpdateRequestSource.NEW_TAB_PAGE); 274 } 275 } 276 277 boolean expectedChangeCompleted = false; 278 if (mPendingRemovalUrl != null && removalCompleted) { 279 mPendingRemovalUrl = null; 280 expectedChangeCompleted = true; 281 } 282 if (mPendingInsertionUrl != null && insertionCompleted) { 283 mPendingInsertionUrl = null; 284 expectedChangeCompleted = true; 285 } 286 287 if (!mHasReceivedData || !mUiDelegate.isVisible() || expectedChangeCompleted) loadTiles(); 288 } 289 290 @Override onIconMadeAvailable(GURL siteUrl)291 public void onIconMadeAvailable(GURL siteUrl) { 292 for (Tile tile : findTilesForUrl(siteUrl)) { 293 mTileRenderer.updateIcon(tile.getData(), 294 new LargeIconCallbackImpl(tile.getData(), /* trackLoadTask = */ false)); 295 } 296 } 297 298 /** 299 * Instructs this instance to start listening for data. The {@link TileGroup.Observer} may be 300 * called immediately if new data is received synchronously. 301 * @param maxResults The maximum number of sites to retrieve. 302 */ startObserving(int maxResults)303 public void startObserving(int maxResults) { 304 addTask(TileTask.FETCH_DATA); 305 mTileGroupDelegate.setMostVisitedSitesObserver(this, maxResults); 306 } 307 308 /** 309 * Method to be called when a tile render has been triggered, to let the {@link TileGroup} 310 * update its internal task tracking status. 311 * @see Delegate#onLoadingComplete(List) 312 */ notifyTilesRendered()313 public void notifyTilesRendered() { 314 // Icon fetch scheduling was done when building the tile views. 315 if (isLoadTracked()) removeTask(TileTask.SCHEDULE_ICON_FETCH); 316 } 317 318 /** @return the sites currently loaded in the group, grouped by vertical. */ getTileSections()319 public SparseArray<List<Tile>> getTileSections() { 320 return mTileSections; 321 } 322 hasReceivedData()323 public boolean hasReceivedData() { 324 return mHasReceivedData; 325 } 326 327 /** @return Whether the group has no sites to display. */ isEmpty()328 public boolean isEmpty() { 329 for (int i = 0; i < mTileSections.size(); i++) { 330 if (!mTileSections.valueAt(i).isEmpty()) return false; 331 } 332 return true; 333 } 334 335 /** 336 * To be called when the view displaying the tile group becomes visible. 337 * @param trackLoadTask whether the delegate should be notified that the load is completed 338 * through {@link Delegate#onLoadingComplete(List)}. 339 */ onSwitchToForeground(boolean trackLoadTask)340 public void onSwitchToForeground(boolean trackLoadTask) { 341 if (trackLoadTask) addTask(TileTask.FETCH_DATA); 342 if (mPendingTiles != null) loadTiles(); 343 if (trackLoadTask) removeTask(TileTask.FETCH_DATA); 344 } 345 346 /** Loads tile data from {@link #mPendingTiles} and clears it afterwards. */ loadTiles()347 private void loadTiles() { 348 assert mPendingTiles != null; 349 350 boolean isInitialLoad = !mHasReceivedData; 351 mHasReceivedData = true; 352 353 boolean dataChanged = isInitialLoad; 354 List<Tile> personalisedTiles = mTileSections.get(TileSectionType.PERSONALIZED); 355 int oldPersonalisedTilesCount = personalisedTiles == null ? 0 : personalisedTiles.size(); 356 357 SparseArray<List<Tile>> newSites = createEmptyTileData(); 358 for (int i = 0; i < mPendingTiles.size(); ++i) { 359 SiteSuggestion suggestion = mPendingTiles.get(i); 360 Tile tile = findTile(suggestion); 361 if (tile == null) { 362 dataChanged = true; 363 tile = new Tile(suggestion, i); 364 } 365 366 List<Tile> sectionTiles = newSites.get(suggestion.sectionType); 367 if (sectionTiles == null) { 368 sectionTiles = new ArrayList<>(); 369 newSites.append(suggestion.sectionType, sectionTiles); 370 } 371 372 // This is not supposed to happen but does. See https://crbug.com/703628 373 if (findTile(suggestion.url, sectionTiles) != null) continue; 374 375 sectionTiles.add(tile); 376 } 377 378 mTileSections = newSites; 379 mPendingTiles = null; 380 381 // TODO(dgn): change these events, maybe introduce new ones or just change semantics? This 382 // will depend on the UI to be implemented and the desired refresh behaviour. 383 List<Tile> personalizedTiles = mTileSections.get(TileSectionType.PERSONALIZED); 384 int numberOfPersonalizedTiles = personalizedTiles == null ? 0 : personalizedTiles.size(); 385 boolean countChanged = 386 isInitialLoad || numberOfPersonalizedTiles != oldPersonalisedTilesCount; 387 dataChanged = dataChanged || countChanged; 388 389 if (!dataChanged) return; 390 391 mOfflineModelObserver.updateAllSuggestionsOfflineAvailability( 392 /* reportPrefetchedSuggestionsCount = */ false); 393 394 if (countChanged) mObserver.onTileCountChanged(); 395 396 if (isLoadTracked()) addTask(TileTask.SCHEDULE_ICON_FETCH); 397 mObserver.onTileDataChanged(); 398 399 if (isInitialLoad) removeTask(TileTask.FETCH_DATA); 400 } 401 402 @Nullable findTile(SiteSuggestion suggestion)403 private Tile findTile(SiteSuggestion suggestion) { 404 if (mTileSections.get(suggestion.sectionType) == null) return null; 405 for (Tile tile : mTileSections.get(suggestion.sectionType)) { 406 if (tile.getData().equals(suggestion)) return tile; 407 } 408 return null; 409 } 410 411 /** 412 * @param url The URL to search for. 413 * @param tiles The section to search in, represented by the contained list of tiles. 414 * @return A tile matching the provided URL and section, or {@code null} if none is found. 415 */ findTile(GURL url, @Nullable List<Tile> tiles)416 private Tile findTile(GURL url, @Nullable List<Tile> tiles) { 417 if (tiles == null) return null; 418 for (Tile tile : tiles) { 419 if (tile.getUrl().equals(url)) return tile; 420 } 421 return null; 422 } 423 424 /** @return All tiles matching the provided URL, or an empty list if none is found. */ findTilesForUrl(GURL url)425 private List<Tile> findTilesForUrl(GURL url) { 426 List<Tile> tiles = new ArrayList<>(); 427 for (int i = 0; i < mTileSections.size(); ++i) { 428 for (Tile tile : mTileSections.valueAt(i)) { 429 if (tile.getUrl().equals(url)) tiles.add(tile); 430 } 431 } 432 return tiles; 433 } 434 addTask(@ileTask int task)435 private void addTask(@TileTask int task) { 436 mPendingTasks.add(task); 437 } 438 removeTask(@ileTask int task)439 private void removeTask(@TileTask int task) { 440 boolean removedTask = mPendingTasks.remove(task); 441 assert removedTask; 442 443 if (mPendingTasks.isEmpty()) { 444 // TODO(dgn): We only notify about the personal tiles because that's the only ones we 445 // wait for to be loaded. We also currently rely on the tile order in the returned 446 // array as the reported position in UMA, but this is not accurate and would be broken 447 // if we returned all the tiles regardless of sections. 448 List<Tile> personalTiles = mTileSections.get(TileSectionType.PERSONALIZED); 449 assert personalTiles != null; 450 mTileGroupDelegate.onLoadingComplete(personalTiles); 451 } 452 } 453 454 /** 455 * @return Whether the current load is being tracked. Unrequested task tracking updates should 456 * not be sent, as it would cause calling {@link Delegate#onLoadingComplete(List)} at the 457 * wrong moment. 458 */ isLoadTracked()459 private boolean isLoadTracked() { 460 return mPendingTasks.contains(TileTask.FETCH_DATA) 461 || mPendingTasks.contains(TileTask.SCHEDULE_ICON_FETCH); 462 } 463 464 @VisibleForTesting isTaskPending(@ileTask int task)465 boolean isTaskPending(@TileTask int task) { 466 return mPendingTasks.contains(task); 467 } 468 469 @VisibleForTesting getTileSetupDelegate()470 TileSetupDelegate getTileSetupDelegate() { 471 return mTileSetupDelegate; 472 } 473 474 @Nullable getHomepageTileData()475 public SiteSuggestion getHomepageTileData() { 476 for (Tile tile : mTileSections.get(TileSectionType.PERSONALIZED)) { 477 if (tile.getSource() == TileSource.HOMEPAGE) { 478 return tile.getData(); 479 } 480 } 481 return null; 482 } 483 createEmptyTileData()484 private static SparseArray<List<Tile>> createEmptyTileData() { 485 SparseArray<List<Tile>> newTileData = new SparseArray<>(); 486 487 // TODO(dgn): How do we want to handle empty states and sections that have no tiles? 488 // Have an empty list for now that can be rendered as-is without causing issues or too much 489 // state checking. We will have to decide if we want empty lists or no section at all for 490 // the others. 491 newTileData.put(TileSectionType.PERSONALIZED, new ArrayList<>()); 492 493 return newTileData; 494 } 495 496 // TODO(dgn): I would like to move that to TileRenderer, but setting the data on the tile, 497 // notifying the observer and updating the tasks make it awkward. 498 private class LargeIconCallbackImpl implements LargeIconBridge.LargeIconCallback { 499 private final SiteSuggestion mSiteData; 500 private final boolean mTrackLoadTask; 501 LargeIconCallbackImpl(SiteSuggestion suggestion, boolean trackLoadTask)502 private LargeIconCallbackImpl(SiteSuggestion suggestion, boolean trackLoadTask) { 503 mSiteData = suggestion; 504 mTrackLoadTask = trackLoadTask; 505 } 506 507 @Override onLargeIconAvailable(@ullable Bitmap icon, int fallbackColor, boolean isFallbackColorDefault, @IconType int iconType)508 public void onLargeIconAvailable(@Nullable Bitmap icon, int fallbackColor, 509 boolean isFallbackColorDefault, @IconType int iconType) { 510 Tile tile = findTile(mSiteData); 511 if (tile != null) { // Do nothing if the tile was removed. 512 tile.setIconType(iconType); 513 if (icon == null) { 514 mTileRenderer.setTileIconFromColor(tile, fallbackColor, isFallbackColorDefault); 515 } else { 516 mTileRenderer.setTileIconFromBitmap(tile, icon); 517 } 518 519 mObserver.onTileIconChanged(tile); 520 } 521 522 // This call needs to be made after the tiles are completely initialised, for UMA. 523 if (mTrackLoadTask) removeTask(TileTask.FETCH_ICON); 524 } 525 } 526 527 private class TileInteractionDelegateImpl 528 implements TileInteractionDelegate, ContextMenuManager.Delegate { 529 private final SiteSuggestion mSuggestion; 530 private Runnable mOnClickRunnable; 531 TileInteractionDelegateImpl(SiteSuggestion suggestion)532 public TileInteractionDelegateImpl(SiteSuggestion suggestion) { 533 mSuggestion = suggestion; 534 } 535 536 @Override onClick(View view)537 public void onClick(View view) { 538 Tile tile = findTile(mSuggestion); 539 if (tile == null) return; 540 541 SuggestionsMetrics.recordTileTapped(); 542 if (mOnClickRunnable != null) mOnClickRunnable.run(); 543 mTileGroupDelegate.openMostVisitedItem(WindowOpenDisposition.CURRENT_TAB, tile); 544 } 545 546 @Override openItem(int windowDisposition)547 public void openItem(int windowDisposition) { 548 Tile tile = findTile(mSuggestion); 549 if (tile == null) return; 550 551 mTileGroupDelegate.openMostVisitedItem(windowDisposition, tile); 552 } 553 554 @Override removeItem()555 public void removeItem() { 556 Tile tile = findTile(mSuggestion); 557 if (tile == null) return; 558 559 // Note: This does not track all the removals, but will track the most recent one. If 560 // that removal is committed, it's good enough for change detection. 561 mPendingRemovalUrl = mSuggestion.url; 562 mTileGroupDelegate.removeMostVisitedItem(tile, url -> mPendingInsertionUrl = url); 563 } 564 565 @Override getUrl()566 public String getUrl() { 567 return mSuggestion.url.getSpec(); 568 } 569 570 @Override getContextMenuTitle()571 public String getContextMenuTitle() { 572 return null; 573 } 574 575 @Override isItemSupported(@ontextMenuItemId int menuItemId)576 public boolean isItemSupported(@ContextMenuItemId int menuItemId) { 577 switch (menuItemId) { 578 // Personalized tiles are the only tiles that can be removed. Additionally, the 579 // Explore tile counts as a personalized tile but cannot be removed. 580 case ContextMenuItemId.REMOVE: 581 return mSuggestion.sectionType == TileSectionType.PERSONALIZED 582 && mSuggestion.source != TileSource.EXPLORE; 583 case ContextMenuItemId.LEARN_MORE: 584 return SuggestionsConfig.scrollToLoad(); 585 case ContextMenuItemId.OPEN_IN_INCOGNITO_TAB: 586 return mSuggestion.source != TileSource.EXPLORE; 587 default: 588 return true; 589 } 590 } 591 592 @Override onContextMenuCreated()593 public void onContextMenuCreated() {} 594 595 @Override onCreateContextMenu( ContextMenu contextMenu, View view, ContextMenuInfo contextMenuInfo)596 public void onCreateContextMenu( 597 ContextMenu contextMenu, View view, ContextMenuInfo contextMenuInfo) { 598 mContextMenuManager.createContextMenu(contextMenu, view, this); 599 } 600 601 @Override setOnClickRunnable(Runnable clickRunnable)602 public void setOnClickRunnable(Runnable clickRunnable) { 603 mOnClickRunnable = clickRunnable; 604 } 605 } 606 607 private class OfflineModelObserver extends SuggestionsOfflineModelObserver<Tile> { OfflineModelObserver(OfflinePageBridge bridge)608 public OfflineModelObserver(OfflinePageBridge bridge) { 609 super(bridge); 610 } 611 612 @Override onSuggestionOfflineIdChanged(Tile tile, OfflinePageItem item)613 public void onSuggestionOfflineIdChanged(Tile tile, OfflinePageItem item) { 614 boolean oldOfflineAvailable = tile.isOfflineAvailable(); 615 tile.setOfflinePageOfflineId(item == null ? null : item.getOfflineId()); 616 617 // Only notify to update the view if there will be a visible change. 618 if (oldOfflineAvailable == tile.isOfflineAvailable()) return; 619 mObserver.onTileOfflineBadgeVisibilityChanged(tile); 620 } 621 622 @Override getOfflinableSuggestions()623 public Iterable<Tile> getOfflinableSuggestions() { 624 List<Tile> tiles = new ArrayList<>(); 625 for (int i = 0; i < mTileSections.size(); ++i) tiles.addAll(mTileSections.valueAt(i)); 626 return tiles; 627 } 628 } 629 } 630