Commit c9f69b5a authored by Tomasz Wiszkowski's avatar Tomasz Wiszkowski Committed by Commit Bot

Early Most Visited Tiles implementation

This change introduces a very simplistic MostVisitedTilesProcessor.
The only functionality wired up at this point is updating the
Omnibox Editing text with the associated URL and very basic keyboard
navigation.

Bug: 1106109
Change-Id: Iac224b2c625eb11d9e1a99ff21465c2b3680b3f2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2437690
Commit-Queue: Tomasz Wiszkowski <ender@google.com>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarFilip Gorski <fgorski@chromium.org>
Cr-Commit-Position: refs/heads/master@{#813039}
parent f9179689
......@@ -1183,6 +1183,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/omnibox/suggestions/header/HeaderView.java",
"java/src/org/chromium/chrome/browser/omnibox/suggestions/header/HeaderViewBinder.java",
"java/src/org/chromium/chrome/browser/omnibox/suggestions/header/HeaderViewProperties.java",
"java/src/org/chromium/chrome/browser/omnibox/suggestions/mostvisited/MostVisitedTilesProcessor.java",
"java/src/org/chromium/chrome/browser/omnibox/suggestions/tail/AlignmentManager.java",
"java/src/org/chromium/chrome/browser/omnibox/suggestions/tail/TailSuggestionProcessor.java",
"java/src/org/chromium/chrome/browser/omnibox/suggestions/tail/TailSuggestionView.java",
......
......@@ -317,6 +317,7 @@
<dimen name="tile_view_bg_corner_radius">2dp</dimen>
<dimen name="tile_view_width">80dp</dimen>
<dimen name="tile_view_width_condensed">64dp</dimen>
<dimen name="tile_view_min_height">86dp</dimen>
<dimen name="tile_view_icon_size">48dp</dimen>
<dimen name="tile_view_icon_size_modern">24dp</dimen>
<!-- TODO(crbug.com/900912): Fix and remove lint ignore -->
......
......@@ -28,11 +28,13 @@ import org.chromium.chrome.browser.omnibox.suggestions.answer.AnswerSuggestionVi
import org.chromium.chrome.browser.omnibox.suggestions.base.BaseSuggestionView;
import org.chromium.chrome.browser.omnibox.suggestions.base.BaseSuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.carousel.BaseCarouselSuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionView;
import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.header.HeaderView;
import org.chromium.chrome.browser.omnibox.suggestions.header.HeaderViewBinder;
import org.chromium.chrome.browser.omnibox.suggestions.mostvisited.MostVisitedTilesProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.tail.TailSuggestionView;
import org.chromium.chrome.browser.omnibox.suggestions.tail.TailSuggestionViewBinder;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler;
......@@ -172,6 +174,11 @@ public class AutocompleteCoordinatorImpl implements AutocompleteCoordinator {
parent -> mQueryTileCoordinator.createView(parent.getContext()),
mQueryTileCoordinator::bind);
adapter.registerType(
OmniboxSuggestionUiType.TILE_NAVSUGGEST,
MostVisitedTilesProcessor::createView,
BaseCarouselSuggestionViewBinder::bind);
adapter.registerType(
OmniboxSuggestionUiType.HEADER,
parent -> new HeaderView(parent.getContext()),
......@@ -287,12 +294,11 @@ public class AutocompleteCoordinatorImpl implements AutocompleteCoordinator {
public boolean handleKeyEvent(int keyCode, KeyEvent event) {
boolean isShowingList = mDropdown != null && mDropdown.getViewGroup().isShown();
boolean isUpOrDown = KeyNavigationUtil.isGoUpOrDown(event);
if (isShowingList && mMediator.getSuggestionCount() > 0 && isUpOrDown) {
boolean isAnyDirection = KeyNavigationUtil.isGoAnyDirection(event);
if (isShowingList && mMediator.getSuggestionCount() > 0 && isAnyDirection) {
mMediator.allowPendingItemSelection();
}
boolean isValidListKey = isUpOrDown || KeyNavigationUtil.isGoRight(event)
|| KeyNavigationUtil.isGoLeft(event) || KeyNavigationUtil.isEnter(event);
boolean isValidListKey = isAnyDirection || KeyNavigationUtil.isEnter(event);
if (isShowingList && isValidListKey && mDropdown.getViewGroup().onKeyDown(keyCode, event)) {
return true;
}
......
......@@ -478,13 +478,6 @@ class AutocompleteMediator implements OnSuggestionsReceivedListener, StartStopWi
public SuggestionViewDelegate createSuggestionViewDelegate(DropdownItemProcessor processor,
PropertyModel model, OmniboxSuggestion suggestion, int position) {
return new SuggestionViewDelegate() {
@Override
public void onSetUrlToSuggestion() {
if (mIgnoreOmniboxItemSelection) return;
mIgnoreOmniboxItemSelection = true;
AutocompleteMediator.this.onSetUrlToSuggestion(suggestion);
}
@Override
public void onSelection() {
processor.recordItemUsed(model);
......@@ -662,10 +655,13 @@ class AutocompleteMediator implements OnSuggestionsReceivedListener, StartStopWi
/**
* Triggered when the user navigates to one of the suggestions without clicking on it.
* @param suggestion The suggestion that was selected.
* @param text The text to be displayed in the Omnibox.
*/
void onSetUrlToSuggestion(OmniboxSuggestion suggestion) {
mDelegate.setOmniboxEditingText(suggestion.getFillIntoEdit());
@Override
public void setOmniboxEditingText(String text) {
if (mIgnoreOmniboxItemSelection) return;
mIgnoreOmniboxItemSelection = true;
mDelegate.setOmniboxEditingText(text);
}
/**
......
......@@ -27,6 +27,7 @@ import org.chromium.chrome.browser.omnibox.suggestions.clipboard.ClipboardSugges
import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.header.HeaderProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.mostvisited.MostVisitedTilesProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.tail.TailSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.tiles.TileSuggestionProcessor;
import org.chromium.chrome.browser.profiles.Profile;
......@@ -100,6 +101,8 @@ class DropdownItemViewInfoListBuilder {
registerSuggestionProcessor(new TailSuggestionProcessor(context, host));
registerSuggestionProcessor(
new TileSuggestionProcessor(context, queryTileSuggestionCallback));
registerSuggestionProcessor(
new MostVisitedTilesProcessor(context, host, iconBridgeSupplier));
registerSuggestionProcessor(
new BasicSuggestionProcessor(context, host, textProvider, iconBridgeSupplier));
}
......
......@@ -13,7 +13,7 @@ import java.lang.annotation.RetentionPolicy;
@IntDef({OmniboxSuggestionUiType.DEFAULT, OmniboxSuggestionUiType.EDIT_URL_SUGGESTION,
OmniboxSuggestionUiType.ANSWER_SUGGESTION, OmniboxSuggestionUiType.ENTITY_SUGGESTION,
OmniboxSuggestionUiType.TAIL_SUGGESTION, OmniboxSuggestionUiType.CLIPBOARD_SUGGESTION,
OmniboxSuggestionUiType.TILE_SUGGESTION})
OmniboxSuggestionUiType.TILE_SUGGESTION, OmniboxSuggestionUiType.TILE_NAVSUGGEST})
@Retention(RetentionPolicy.SOURCE)
public @interface OmniboxSuggestionUiType {
int DEFAULT = 0;
......@@ -24,4 +24,5 @@ public @interface OmniboxSuggestionUiType {
int CLIPBOARD_SUGGESTION = 5;
int TILE_SUGGESTION = 6;
int HEADER = 7;
int TILE_NAVSUGGEST = 8;
}
......@@ -40,4 +40,11 @@ public interface SuggestionHost {
* @param isCollapsed True if group should appear collapsed, otherwise false.
*/
void setGroupCollapsedState(int groupId, boolean isCollapsed);
/**
* Update the content of the Omnibox without triggering the Navigation.
*
* @param text The text to be displayed in the Omnibox.
*/
void setOmniboxEditingText(String text);
}
......@@ -13,7 +13,4 @@ public interface SuggestionViewDelegate {
/** Triggered when the user long presses the omnibox suggestion. */
void onLongPress();
/** Triggered when the user navigates to one of the suggestions without clicking on it. */
void onSetUrlToSuggestion();
}
......@@ -11,6 +11,7 @@ import android.view.View;
import android.widget.ImageView;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.AppCompatImageView;
......@@ -32,6 +33,7 @@ public class BaseSuggestionView<T extends View> extends SimpleHorizontalLayoutVi
private final List<ImageView> mActionButtons;
private final DecoratedSuggestionView<T> mDecoratedView;
private SuggestionViewDelegate mDelegate;
private @Nullable Runnable mOnFocusViaSelectionListener;
/**
* Constructs a new suggestion view.
......@@ -136,8 +138,8 @@ public class BaseSuggestionView<T extends View> extends SimpleHorizontalLayoutVi
@Override
public void setSelected(boolean selected) {
mDecoratedView.setSelected(selected);
if (selected) {
mDelegate.onSetUrlToSuggestion();
if (selected && mOnFocusViaSelectionListener != null) {
mOnFocusViaSelectionListener.run();
}
}
......@@ -170,6 +172,15 @@ public class BaseSuggestionView<T extends View> extends SimpleHorizontalLayoutVi
mDelegate = delegate;
}
/**
* Specify the listener receiving a call when the user highlights this Suggestion.
*
* @param listener The listener to be notified about selection.
*/
void setOnFocusViaSelectionListener(@Nullable Runnable listener) {
mOnFocusViaSelectionListener = listener;
}
/** @return Widget holding suggestion decoration icon. */
RoundedCornerImageView getSuggestionImageView() {
return mDecoratedView.getImageView();
......
......@@ -63,6 +63,9 @@ public final class BaseSuggestionViewBinder<T extends View>
updateColorScheme(model, view);
} else if (BaseSuggestionViewProperties.ACTIONS == propertyKey) {
bindActionButtons(model, view, model.get(BaseSuggestionViewProperties.ACTIONS));
} else if (BaseSuggestionViewProperties.ON_FOCUS_VIA_SELECTION == propertyKey) {
view.setOnFocusViaSelectionListener(
model.get(BaseSuggestionViewProperties.ON_FOCUS_VIA_SELECTION));
}
}
......
......@@ -152,6 +152,8 @@ public abstract class BaseSuggestionViewProcessor implements SuggestionProcessor
mSuggestionHost.createSuggestionViewDelegate(this, model, suggestion, position);
model.set(BaseSuggestionViewProperties.SUGGESTION_DELEGATE, delegate);
model.set(BaseSuggestionViewProperties.ON_FOCUS_VIA_SELECTION,
() -> { mSuggestionHost.setOmniboxEditingText(suggestion.getFillIntoEdit()); });
model.set(BaseSuggestionViewProperties.DENSITY, mDensity);
setCustomActions(model, null);
}
......
......@@ -74,6 +74,10 @@ public class BaseSuggestionViewProperties {
public static final WritableObjectPropertyKey<List<Action>> ACTIONS =
new WritableObjectPropertyKey();
/** Callback invoked when the Suggestion view is highlighted. */
public static final WritableObjectPropertyKey<Runnable> ON_FOCUS_VIA_SELECTION =
new WritableObjectPropertyKey<>();
/** Delegate receiving user events. */
public static final WritableObjectPropertyKey<SuggestionViewDelegate> SUGGESTION_DELEGATE =
new WritableObjectPropertyKey<>();
......@@ -82,7 +86,7 @@ public class BaseSuggestionViewProperties {
public static final WritableIntPropertyKey DENSITY = new WritableIntPropertyKey();
public static final PropertyKey[] ALL_UNIQUE_KEYS =
new PropertyKey[] {ACTIONS, ICON, DENSITY, SUGGESTION_DELEGATE};
new PropertyKey[] {ACTIONS, ICON, DENSITY, SUGGESTION_DELEGATE, ON_FOCUS_VIA_SELECTION};
public static final PropertyKey[] ALL_KEYS =
PropertyModel.concatKeys(ALL_UNIQUE_KEYS, SuggestionCommonProperties.ALL_KEYS);
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.omnibox.OmniboxSuggestionType;
import org.chromium.chrome.browser.omnibox.styles.OmniboxResourceProvider;
import org.chromium.chrome.browser.omnibox.styles.OmniboxTheme;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestion;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionUiType;
import org.chromium.chrome.browser.omnibox.suggestions.SuggestionHost;
import org.chromium.chrome.browser.omnibox.suggestions.carousel.BaseCarouselSuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.carousel.BaseCarouselSuggestionView;
import org.chromium.chrome.browser.omnibox.suggestions.carousel.BaseCarouselSuggestionViewProperties;
import org.chromium.chrome.browser.suggestions.tile.TileView;
import org.chromium.chrome.browser.suggestions.tile.TileViewBinder;
import org.chromium.chrome.browser.suggestions.tile.TileViewProperties;
import org.chromium.chrome.browser.ui.favicon.LargeIconBridge;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.SimpleRecyclerViewAdapter;
import org.chromium.url.GURL;
import java.util.ArrayList;
import java.util.List;
/**
* SuggestionProcessor for Most Visited URL tiles.
* TODO(crbug.com/1106109): Write integration tests.
*/
public class MostVisitedTilesProcessor extends BaseCarouselSuggestionProcessor {
private final @NonNull Context mContext;
private final @NonNull SuggestionHost mSuggestionHost;
private final @NonNull Supplier<LargeIconBridge> mIconBridgeSupplier;
private final int mMinCarouselItemViewHeight;
private static final int DEFAULT_TILE_TYPE = 0;
/**
* Constructor.
*
* @param context An Android context.
* @param host SuggestionHost receiving notifications about user actions.
* @param iconBridgeSupplier Supplier of the LargeIconBridge used to fetch site favicons.
*/
public MostVisitedTilesProcessor(@NonNull Context context, @NonNull SuggestionHost host,
@NonNull Supplier<LargeIconBridge> iconBridgeSupplier) {
super(context);
mContext = context;
mSuggestionHost = host;
mIconBridgeSupplier = iconBridgeSupplier;
mMinCarouselItemViewHeight =
context.getResources().getDimensionPixelSize(R.dimen.tile_view_min_height);
}
@Override
public boolean doesProcessSuggestion(OmniboxSuggestion suggestion, int position) {
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 populateModel(OmniboxSuggestion suggestion, PropertyModel model, int position) {
final List<OmniboxSuggestion.NavsuggestTile> tiles = suggestion.getNavsuggestTiles();
final int tilesCount = tiles.size();
final List<ListItem> tileList = new ArrayList<>(tilesCount);
final LargeIconBridge iconBridge = mIconBridgeSupplier.get();
for (int index = 0; index < tilesCount; index++) {
final PropertyModel tileModel = new PropertyModel(TileViewProperties.ALL_KEYS);
final GURL url = tiles.get(index).url;
tileModel.set(TileViewProperties.TITLE, tiles.get(index).title);
tileModel.set(TileViewProperties.TITLE_LINES, 1);
tileModel.set(TileViewProperties.ON_FOCUS_VIA_SELECTION,
() -> { mSuggestionHost.setOmniboxEditingText(url.getSpec()); });
if (iconBridge != null) {
iconBridge.getLargeIconForUrl(tiles.get(index).url, /* size */ 32,
(Bitmap icon, int fallbackColor, boolean isFallbackColorDefault,
int iconType) -> {
if (icon == null) return;
tileModel.set(TileViewProperties.ICON, new BitmapDrawable(icon));
});
}
tileList.add(new ListItem(DEFAULT_TILE_TYPE, tileModel));
}
model.set(BaseCarouselSuggestionViewProperties.TILES, tileList);
model.set(BaseCarouselSuggestionViewProperties.TITLE,
mContext.getResources().getString(R.string.most_visited_tiles_header));
}
/**
* Create Carousel Suggestion View presenting the Most Visited URL tiles.
*
* @param parent ViewGroup that will host the Carousel view.
* @return BaseCarouselSuggestionView for the Most Visited URL suggestions.
*/
public static BaseCarouselSuggestionView createView(ViewGroup parent) {
SimpleRecyclerViewAdapter adapter = new SimpleRecyclerViewAdapter(new ModelList());
adapter.registerType(
DEFAULT_TILE_TYPE, MostVisitedTilesProcessor::buildTile, TileViewBinder::bind);
return new BaseCarouselSuggestionView(parent.getContext(), adapter);
}
/**
* Create a Tile element for the Most Visited URL suggestions.
*
* @param parent ViewGroup that will host the Tile.
* @return A TileView element for the individual URL suggestion.
*/
private static TileView buildTile(ViewGroup parent) {
TileView tile = (TileView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.suggestions_tile_view, parent, false);
tile.setClickable(true);
Drawable background = OmniboxResourceProvider.resolveAttributeToDrawable(
parent.getContext(), OmniboxTheme.LIGHT_THEME, R.attr.selectableItemBackground);
tile.setBackgroundDrawable(background);
return tile;
}
}
......@@ -40,6 +40,9 @@ public class BaseSuggestionViewTest {
private View mDecoratedView;
private View mContentView;
@Mock
private Runnable mOnFocusListener;
@Mock
SuggestionViewDelegate mMockDelegate;
......@@ -87,6 +90,7 @@ public class BaseSuggestionViewTest {
mContentView = new View(mActivity);
mView = new BaseSuggestionViewForTest(mContentView);
mView.setDelegate(mMockDelegate);
mView.setOnFocusViaSelectionListener(mOnFocusListener);
mActionIconWidthPx = mActivity.getResources().getDimensionPixelSize(
R.dimen.omnibox_suggestion_action_icon_width);
......@@ -333,12 +337,12 @@ public class BaseSuggestionViewTest {
@Test
public void setSelected_emitsOmniboxUpdateWhenSelected() {
mView.setSelected(true);
verify(mMockDelegate, times(1)).onSetUrlToSuggestion();
verify(mOnFocusListener, times(1)).run();
}
@Test
public void setSelected_noOmniboxUpdateWhenDeselected() {
mView.setSelected(false);
verify(mMockDelegate, never()).onSetUrlToSuggestion();
verify(mOnFocusListener, never()).run();
}
}
......@@ -72,6 +72,15 @@ public class KeyNavigationUtil {
return isGoDown(event) || isGoUp(event);
}
/**
* Checks whether the given event is any DPAD or NUMPAD direction.
* @param event Event to be checked.
* @return Whether the event should be processed as any of navigation direction.
*/
public static boolean isGoAnyDirection(KeyEvent event) {
return isGoDown(event) || isGoUp(event) || isGoLeft(event) || isGoRight(event);
}
/**
* Checks whether the given event is any of ENTER or NUMPAD ENTER.
* @param event Event to be checked.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment