blob: adc743f424cc5106af6afbd3983d05625f606bcd [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.omnibox.suggestions.mostvisited;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.omnibox.OmniboxFeatures;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.chrome.browser.omnibox.styles.OmniboxResourceProvider;
import org.chromium.chrome.browser.omnibox.suggestions.FaviconFetcher;
import org.chromium.chrome.browser.omnibox.suggestions.SuggestionCommonProperties;
import org.chromium.chrome.browser.omnibox.suggestions.SuggestionHost;
import org.chromium.chrome.browser.omnibox.suggestions.carousel.BaseCarouselSuggestionItemViewBuilder;
import org.chromium.chrome.browser.omnibox.suggestions.carousel.BaseCarouselSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.carousel.BaseCarouselSuggestionViewProperties;
import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.widget.tile.TileViewProperties;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.AutocompleteMatch.SuggestTile;
import org.chromium.components.omnibox.OmniboxMetrics;
import org.chromium.components.omnibox.OmniboxSuggestionType;
import org.chromium.components.omnibox.suggestions.OmniboxSuggestionUiType;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;
import java.util.ArrayList;
import java.util.List;
/**
* SuggestionProcessor for Most Visited URL tiles.
*/
public class MostVisitedTilesProcessor extends BaseCarouselSuggestionProcessor {
/**
* RecyclerView pool that adds the max recycled view for the most visited tile carousel and
* avoid re-creating views until we're sure we won't be needing them.
*/
private class MostVisitedTilesRecycledViewPool extends RecycledViewPool {
public MostVisitedTilesRecycledViewPool() {
setMaxRecycledViews(OmniboxSuggestionUiType.DEFAULT, 10);
}
}
private final @NonNull Context mContext;
private final @NonNull SuggestionHost mSuggestionHost;
private final @NonNull FaviconFetcher mFaviconFetcher;
private final int mMinCarouselItemViewHeight;
private @Nullable RecycledViewPool mMostVisitedTilesRecycledViewPool;
private boolean mEnableOrganicRepeatableQueries;
/**
* Constructor.
*
* @param context An Android context.
* @param host SuggestionHost receiving notifications about user actions.
* @param faviconFetcher Class retrieving favicons for the MV Tiles.
*/
public MostVisitedTilesProcessor(@NonNull Context context, @NonNull SuggestionHost host,
@NonNull FaviconFetcher faviconFetcher) {
super(context);
mContext = context;
mSuggestionHost = host;
mFaviconFetcher = faviconFetcher;
mMinCarouselItemViewHeight =
mContext.getResources().getDimensionPixelSize(R.dimen.tile_view_min_height);
}
@Override
public boolean doesProcessSuggestion(AutocompleteMatch suggestion, int matchIndex) {
return suggestion.getType() == OmniboxSuggestionType.TILE_NAVSUGGEST;
}
@Override
public int getViewTypeId() {
return OmniboxSuggestionUiType.TILE_NAVSUGGEST;
}
@Override
public PropertyModel createModel() {
return new PropertyModel(BaseCarouselSuggestionViewProperties.ALL_KEYS);
}
@Override
public int getMinimumCarouselItemViewHeight() {
return mMinCarouselItemViewHeight;
}
@Override
public void onNativeInitialized() {
super.onNativeInitialized();
mEnableOrganicRepeatableQueries =
ChromeFeatureList.isEnabled(ChromeFeatureList.HISTORY_ORGANIC_REPEATABLE_QUERIES);
// Initialize a recycled view pool for the most visited tiles carousel to reduce unnecessary
// fetching and jankiness.
if (OmniboxFeatures.shouldAddMostVisitedTilesRecycledViewPool()) {
mMostVisitedTilesRecycledViewPool = new MostVisitedTilesRecycledViewPool();
}
}
@Override
public void populateModel(AutocompleteMatch suggestion, PropertyModel model, int matchIndex) {
super.populateModel(suggestion, model, matchIndex);
final List<AutocompleteMatch.SuggestTile> tiles = suggestion.getSuggestTiles();
final int tilesCount = tiles.size();
final List<ListItem> tileList = new ArrayList<>(tilesCount);
for (int elementIndex = 0; elementIndex < tilesCount; elementIndex++) {
final PropertyModel tileModel = new PropertyModel(TileViewProperties.ALL_KEYS);
final SuggestTile tile = tiles.get(elementIndex);
// Use website host text when the website title is empty (for example: gmail.com).
final String title = TextUtils.isEmpty(tile.title) ? tile.url.getHost() : tile.title;
final GURL url = tile.url;
final boolean isSearch = tile.isSearch && mEnableOrganicRepeatableQueries;
final int itemIndex = elementIndex;
tileModel.set(TileViewProperties.TITLE, title);
tileModel.set(TileViewProperties.TITLE_LINES, 1);
tileModel.set(TileViewProperties.ON_FOCUS_VIA_SELECTION,
() -> mSuggestionHost.setOmniboxEditingText(url.getSpec()));
tileModel.set(TileViewProperties.ON_CLICK, v -> {
OmniboxMetrics.recordSuggestTileTypeUsed(itemIndex, isSearch);
mSuggestionHost.onSuggestionClicked(suggestion, matchIndex, url);
});
final int elementIndexForDeletion = elementIndex;
tileModel.set(TileViewProperties.ON_LONG_CLICK, v -> {
mSuggestionHost.onDeleteMatchElement(
suggestion, title, matchIndex, elementIndexForDeletion);
return true;
});
if (isSearch) {
// Note: we should never show most visited tiles in incognito mode. Catch this early
// if we ever do.
assert model.get(SuggestionCommonProperties.COLOR_SCHEME)
!= BrandedColorScheme.INCOGNITO;
tileModel.set(TileViewProperties.ICON_TINT,
ChromeColors.getSecondaryIconTint(mContext, /* isIncognito= */ false));
tileModel.set(TileViewProperties.ICON,
OmniboxResourceProvider.getDrawable(
mContext, R.drawable.ic_suggestion_magnifier));
tileModel.set(TileViewProperties.CONTENT_DESCRIPTION,
OmniboxResourceProvider.getString(mContext,
R.string.accessibility_omnibox_most_visited_tile_search, title));
} else {
tileModel.set(TileViewProperties.ICON_TINT, null);
tileModel.set(TileViewProperties.CONTENT_DESCRIPTION,
OmniboxResourceProvider.getString(mContext,
R.string.accessibility_omnibox_most_visited_tile_navigate, title,
url.getHost()));
tileModel.set(TileViewProperties.SMALL_ICON_ROUNDING_RADIUS,
mContext.getResources().getDimensionPixelSize(
R.dimen.omnibox_carousel_icon_rounding_radius));
mFaviconFetcher.fetchFaviconWithBackoff(url, true, (icon, type) -> {
tileModel.set(TileViewProperties.ICON, new BitmapDrawable(icon));
});
}
tileList.add(new ListItem(
BaseCarouselSuggestionItemViewBuilder.ViewType.TILE_VIEW, tileModel));
}
model.set(BaseCarouselSuggestionViewProperties.TILES, tileList);
model.set(BaseCarouselSuggestionViewProperties.SHOW_TITLE, false);
model.set(BaseCarouselSuggestionViewProperties.RECYCLED_VIEW_POOL,
mMostVisitedTilesRecycledViewPool);
}
/**
* Respond to URL bar focus change.
*
* @param hasFocus Indicates whether URL bar is now focused.
*/
@Override
public void onUrlFocusChange(boolean hasFocus) {
// Clear the Recycled View Pool when the omnibox loses focus.
if (!hasFocus && mMostVisitedTilesRecycledViewPool != null) {
mMostVisitedTilesRecycledViewPool.clear();
}
}
}