blob: ded877ece2840ffdea8e435249171c4966d95da2 [file] [log] [blame]
// Copyright 2018 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.editurl;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.IntDef;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import org.chromium.base.metrics.CachedMetrics;
import org.chromium.base.metrics.CachedMetrics.EnumeratedHistogramSample;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.favicon.LargeIconBridge;
import org.chromium.chrome.browser.omnibox.OmniboxSuggestionType;
import org.chromium.chrome.browser.omnibox.UrlBar;
import org.chromium.chrome.browser.omnibox.UrlBar.OmniboxAction;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestion;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionUiType;
import org.chromium.chrome.browser.omnibox.suggestions.SuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionHost;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.share.ShareMenuActionHandler;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.ui.base.Clipboard;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* This class controls the interaction of the "edit url" suggestion item with the rest of the
* suggestions list. This class also serves as a mediator, containing logic that interacts with
* the rest of Chrome.
*/
public class EditUrlSuggestionProcessor implements OnClickListener, SuggestionProcessor {
/** An interface for handling taps on the suggestion rather than its buttons. */
public interface SuggestionSelectionHandler {
/**
* Handle the edit URL suggestion selection.
* @param suggestion The selected suggestion.
*/
void onEditUrlSuggestionSelected(OmniboxSuggestion suggestion);
}
/** An interface for modifying the location bar and it's contents. */
public interface LocationBarDelegate {
/** Remove focus from the omnibox. */
void clearOmniboxFocus();
/**
* Set the text in the omnibox.
* @param text The text that should be displayed in the omnibox.
*/
void setOmniboxEditingText(String text);
}
/** The actions that can be performed on the suggestion view provided by this class. */
@IntDef({SuggestionAction.EDIT, SuggestionAction.COPY, SuggestionAction.SHARE,
SuggestionAction.TAP})
@Retention(RetentionPolicy.SOURCE)
public @interface SuggestionAction {
int EDIT = 0;
int COPY = 1;
int SHARE = 2;
int TAP = 3;
int NUM_ENTRIES = 4;
}
/** Cached metrics in the event this code is triggered prior to native being initialized. */
private static final EnumeratedHistogramSample ENUMERATED_SUGGESTION_ACTION =
new EnumeratedHistogramSample(
"Omnibox.EditUrlSuggestionAction", SuggestionAction.NUM_ENTRIES);
private static final CachedMetrics.ActionEvent ACTION_EDIT_URL_SUGGESTION_TAP =
new CachedMetrics.ActionEvent("Omnibox.EditUrlSuggestion.Tap");
private static final CachedMetrics.ActionEvent ACTION_EDIT_URL_SUGGESTION_COPY =
new CachedMetrics.ActionEvent("Omnibox.EditUrlSuggestion.Copy");
private static final CachedMetrics.ActionEvent ACTION_EDIT_URL_SUGGESTION_EDIT =
new CachedMetrics.ActionEvent("Omnibox.EditUrlSuggestion.Edit");
private static final CachedMetrics.ActionEvent ACTION_EDIT_URL_SUGGESTION_SHARE =
new CachedMetrics.ActionEvent("Omnibox.EditUrlSuggestion.Share");
/** The delegate for accessing the location bar for observation and modification. */
private final LocationBarDelegate mLocationBarDelegate;
/** A means of accessing the activity's tab. */
private ActivityTabProvider mTabProvider;
/** Whether the omnibox has already cleared its content for the focus event. */
private boolean mHasClearedOmniboxForFocus;
/** The last omnibox suggestion to be processed. */
private OmniboxSuggestion mLastProcessedSuggestion;
/** A handler for suggestion selection. */
private SuggestionSelectionHandler mSelectionHandler;
/** The original URL that was in the omnibox when it was focused. */
private String mOriginalUrl;
/** The original title of the page. */
private String mOriginalTitle;
/** The last time that the omnibox was focused. */
private long mLastOmniboxFocusTime;
/** Whether a timing event should be recorded. This will be true once per omnibox focus. */
private boolean mShouldRecordTimingEvent;
/** Whether the first suggestion after an omnibox focus event has been processed. */
private boolean mFirstSuggestionProcessedForCurrentOmniboxFocus;
/** Whether this processor should ignore all subsequent suggestion. */
private boolean mIgnoreSuggestions;
/** Whether suggestion site favicons are enabled. */
private boolean mEnableSuggestionFavicons;
/** Edge size (in pixels) of the favicon. Used to request best matching favicon from cache. */
private final int mDesiredFaviconWidthPx;
/** Supplies Profile information. */
private Profile mCurrentUserProfile;
/** Supplies site favicons. */
private LargeIconBridge mLargeIconBridge;
/** Supplies additional control over suggestion model. */
private final SuggestionHost mSuggestionHost;
/**
* @param locationBarDelegate A means of modifying the location bar.
* @param selectionHandler A mechanism for handling selection of the edit URL suggestion item.
*/
public EditUrlSuggestionProcessor(Context context, SuggestionHost suggestionHost,
LocationBarDelegate locationBarDelegate, SuggestionSelectionHandler selectionHandler) {
mLocationBarDelegate = locationBarDelegate;
mSelectionHandler = selectionHandler;
mDesiredFaviconWidthPx = context.getResources().getDimensionPixelSize(
R.dimen.omnibox_suggestion_favicon_size);
mSuggestionHost = suggestionHost;
}
/**
* Create the view specific to the suggestion this processor is responsible for.
* @param context An Android context.
* @return An edit-URL suggestion view.
*/
public static ViewGroup createView(Context context) {
LayoutInflater inflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return (ViewGroup) inflater.inflate(R.layout.edit_url_suggestion_layout, null);
}
@Override
public boolean doesProcessSuggestion(OmniboxSuggestion suggestion) {
Tab activeTab = mTabProvider != null ? mTabProvider.get() : null;
// The what-you-typed suggestion can potentially appear as the second suggestion in some
// cases. If the first suggestion isn't the one we want, ignore all subsequent suggestions.
if (!mFirstSuggestionProcessedForCurrentOmniboxFocus) {
mFirstSuggestionProcessedForCurrentOmniboxFocus = true;
mIgnoreSuggestions = activeTab == null || activeTab.isNativePage()
|| activeTab.isIncognito()
|| OmniboxSuggestionType.URL_WHAT_YOU_TYPED != suggestion.getType()
|| !TextUtils.equals(suggestion.getUrl(), activeTab.getUrl());
}
if (OmniboxSuggestionType.URL_WHAT_YOU_TYPED != suggestion.getType()
|| mIgnoreSuggestions) {
return false;
}
mLastProcessedSuggestion = suggestion;
// Only use the URL provided by the "what you typed" suggestion on first omnibox focus.
// Subsequent suggestions will provide partial URLs which we do not want. If the suggestion
// URL matches the original, show the suggestion item.
if (mOriginalUrl == null) mOriginalUrl = mLastProcessedSuggestion.getUrl();
if (!TextUtils.equals(mLastProcessedSuggestion.getUrl(), mOriginalUrl)) return false;
if (!mHasClearedOmniboxForFocus) {
mHasClearedOmniboxForFocus = true;
mLocationBarDelegate.setOmniboxEditingText("");
}
return true;
}
@Override
public int getViewTypeId() {
return OmniboxSuggestionUiType.EDIT_URL_SUGGESTION;
}
@Override
public PropertyModel createModelForSuggestion(OmniboxSuggestion suggestion) {
return new PropertyModel(EditUrlSuggestionProperties.ALL_KEYS);
}
@Override
public void populateModel(OmniboxSuggestion suggestion, PropertyModel model, int position) {
model.set(EditUrlSuggestionProperties.TEXT_CLICK_LISTENER, this);
model.set(EditUrlSuggestionProperties.BUTTON_CLICK_LISTENER, this);
// Lazily create LargeIconBridge in case Profile is reported ahead on Native initialized.
if (mEnableSuggestionFavicons && mLargeIconBridge == null && mCurrentUserProfile != null) {
mLargeIconBridge = new LargeIconBridge(mCurrentUserProfile);
}
if (mLargeIconBridge != null) {
mLargeIconBridge.getLargeIconForUrl(mLastProcessedSuggestion.getUrl(),
mDesiredFaviconWidthPx,
(Bitmap icon, int fallbackColor, boolean isFallbackColorDefault,
int iconType) -> {
if (!mSuggestionHost.isActiveModel(model)) return;
model.set(EditUrlSuggestionProperties.SITE_FAVICON, icon);
mSuggestionHost.notifyPropertyModelsChanged();
});
}
if (mOriginalTitle == null) mOriginalTitle = mTabProvider.get().getTitle();
model.set(EditUrlSuggestionProperties.TITLE_TEXT, mOriginalTitle);
model.set(EditUrlSuggestionProperties.URL_TEXT, mLastProcessedSuggestion.getUrl());
}
@Override
public void onNativeInitialized() {
mEnableSuggestionFavicons =
ChromeFeatureList.isEnabled(ChromeFeatureList.OMNIBOX_SHOW_SUGGESTION_FAVICONS);
}
/**
* Updates the profile used for extracting website favicons.
* @param profile The profile to be used.
*/
public void setProfile(Profile profile) {
if (mCurrentUserProfile == profile) return;
mCurrentUserProfile = profile;
mLargeIconBridge = null;
}
/**
* @param provider A means of accessing the activity's tab.
*/
public void setActivityTabProvider(ActivityTabProvider provider) {
mTabProvider = provider;
}
/**
* Clean up any state that this coordinator has.
*/
public void destroy() {
mLastProcessedSuggestion = null;
mSelectionHandler = null;
}
@Override
public void onUrlFocusChange(boolean hasFocus) {
if (hasFocus) {
mLastOmniboxFocusTime = System.currentTimeMillis();
} else {
mOriginalUrl = null;
mOriginalTitle = null;
mHasClearedOmniboxForFocus = false;
mLastProcessedSuggestion = null;
mFirstSuggestionProcessedForCurrentOmniboxFocus = false;
mIgnoreSuggestions = false;
}
mShouldRecordTimingEvent = hasFocus;
}
@Override
public void onClick(View view) {
Tab activityTab = mTabProvider.get();
assert activityTab != null : "A tab is required to make changes to the location bar.";
if (R.id.url_copy_icon == view.getId()) {
ENUMERATED_SUGGESTION_ACTION.record(SuggestionAction.COPY);
ACTION_EDIT_URL_SUGGESTION_COPY.record();
if (mShouldRecordTimingEvent) {
UrlBar.recordTimedActionForMetrics(OmniboxAction.COPY, mLastOmniboxFocusTime);
mShouldRecordTimingEvent = false;
}
Clipboard.getInstance().copyUrlToClipboard(mLastProcessedSuggestion.getUrl());
} else if (R.id.url_share_icon == view.getId()) {
ENUMERATED_SUGGESTION_ACTION.record(SuggestionAction.SHARE);
ACTION_EDIT_URL_SUGGESTION_SHARE.record();
if (mShouldRecordTimingEvent) {
UrlBar.recordTimedActionForMetrics(OmniboxAction.SHARE, mLastOmniboxFocusTime);
mShouldRecordTimingEvent = false;
}
mLocationBarDelegate.clearOmniboxFocus();
// TODO(mdjones): This should only share the displayed URL instead of the background
// tab.
ShareMenuActionHandler.getInstance().onShareMenuItemSelected(
activityTab.getActivity(), activityTab, false, activityTab.isIncognito());
} else if (R.id.url_edit_icon == view.getId()) {
ENUMERATED_SUGGESTION_ACTION.record(SuggestionAction.EDIT);
ACTION_EDIT_URL_SUGGESTION_EDIT.record();
mLocationBarDelegate.setOmniboxEditingText(mLastProcessedSuggestion.getUrl());
} else {
ENUMERATED_SUGGESTION_ACTION.record(SuggestionAction.TAP);
ACTION_EDIT_URL_SUGGESTION_TAP.record();
// If the event wasn't on any of the buttons, treat is as a tap on the general
// suggestion.
if (mSelectionHandler != null) {
mSelectionHandler.onEditUrlSuggestionSelected(mLastProcessedSuggestion);
}
}
}
}