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