blob: 14e9783f7067c0ca8c8caa5f28d1fd378fd80acf [file] [log] [blame]
// Copyright 2019 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.answer;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Pair;
import android.util.TypedValue;
import android.view.View;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.omnibox.OmniboxSuggestionType;
import org.chromium.chrome.browser.omnibox.UrlBarEditingTextStateProvider;
import org.chromium.chrome.browser.omnibox.suggestions.AnswersImageFetcher;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteCoordinator.SuggestionProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestion;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionUiType;
import org.chromium.chrome.browser.omnibox.suggestions.answer.AnswerSuggestionViewProperties.AnswerIcon;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionHost;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewDelegate;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewProperties;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewProperties.SuggestionIcon;
import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewProperties.SuggestionTextContainer;
import org.chromium.components.omnibox.AnswerType;
import org.chromium.components.omnibox.SuggestionAnswer;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** A class that handles model and view creation for the most commonly used omnibox suggestion. */
public class AnswerSuggestionProcessor implements SuggestionProcessor {
private final Map<String, List<PropertyModel>> mPendingAnswerRequestUrls;
private final Context mContext;
private final SuggestionHost mSuggestionHost;
private final AnswersImageFetcher mImageFetcher;
private final UrlBarEditingTextStateProvider mUrlBarEditingTextProvider;
private boolean mEnableNewAnswerLayout;
/**
* @param context An Android context.
* @param suggestionHost A handle to the object using the suggestions.
*/
public AnswerSuggestionProcessor(Context context, SuggestionHost suggestionHost,
UrlBarEditingTextStateProvider editingTextProvider) {
mContext = context;
mSuggestionHost = suggestionHost;
mPendingAnswerRequestUrls = new HashMap<>();
mImageFetcher = new AnswersImageFetcher();
mUrlBarEditingTextProvider = editingTextProvider;
}
@Override
public boolean doesProcessSuggestion(OmniboxSuggestion suggestion) {
// Calculation answers are specific in a way that these are basic suggestions, but processed
// as answers, when new answer layout is enabled.
return suggestion.hasAnswer() || suggestion.getType() == OmniboxSuggestionType.CALCULATOR;
}
@Override
public void onNativeInitialized() {
// Experiment: controls presence of certain answer icon types.
mEnableNewAnswerLayout =
ChromeFeatureList.isEnabled(ChromeFeatureList.OMNIBOX_NEW_ANSWER_LAYOUT);
}
@Override
public int getViewTypeId() {
return mEnableNewAnswerLayout ? OmniboxSuggestionUiType.ANSWER_SUGGESTION
: OmniboxSuggestionUiType.DEFAULT;
}
@Override
public PropertyModel createModelForSuggestion(OmniboxSuggestion suggestion) {
return mEnableNewAnswerLayout ? new PropertyModel(AnswerSuggestionViewProperties.ALL_KEYS)
: new PropertyModel(SuggestionViewProperties.ALL_KEYS);
}
@Override
public void populateModel(OmniboxSuggestion suggestion, PropertyModel model, int position) {
maybeFetchAnswerIcon(suggestion, model);
SuggestionViewDelegate delegate =
mSuggestionHost.createSuggestionViewDelegate(suggestion, position);
if (mEnableNewAnswerLayout) {
setStateForNewSuggestion(model, suggestion, delegate);
} else {
setStateForClassicSuggestion(model, suggestion, delegate);
}
}
@Override
public void onUrlFocusChange(boolean hasFocus) {
if (!hasFocus) mImageFetcher.clearCache();
}
private void maybeFetchAnswerIcon(OmniboxSuggestion suggestion, PropertyModel model) {
ThreadUtils.assertOnUiThread();
// Attempting to fetch answer data before we have a profile to request it for.
if (mSuggestionHost.getCurrentProfile() == null) return;
// Note: we also handle calculations here, which do not have answer defined.
if (!suggestion.hasAnswer()) return;
final String url = suggestion.getAnswer().getSecondLine().getImage();
if (url == null) return;
// Do not make duplicate answer image requests for the same URL (to avoid generating
// duplicate bitmaps for the same image).
if (mPendingAnswerRequestUrls.containsKey(url)) {
mPendingAnswerRequestUrls.get(url).add(model);
return;
}
List<PropertyModel> models = new ArrayList<>();
models.add(model);
mPendingAnswerRequestUrls.put(url, models);
mImageFetcher.requestAnswersImage(mSuggestionHost.getCurrentProfile(), url,
new AnswersImageFetcher.AnswersImageObserver() {
@Override
public void onAnswersImageChanged(Bitmap bitmap) {
ThreadUtils.assertOnUiThread();
List<PropertyModel> models = mPendingAnswerRequestUrls.remove(url);
boolean didUpdate = false;
for (int i = 0; i < models.size(); i++) {
PropertyModel model = models.get(i);
if (!mSuggestionHost.isActiveModel(model)) continue;
if (mEnableNewAnswerLayout) {
model.set(AnswerSuggestionViewProperties.ANSWER_IMAGE, bitmap);
} else {
model.set(SuggestionViewProperties.ANSWER_IMAGE, bitmap);
}
didUpdate = true;
}
if (didUpdate) mSuggestionHost.notifyPropertyModelsChanged();
}
});
}
/**
* Sets both lines of the Omnibox suggestion in a basic Suggestion result.
*/
private void setStateForClassicSuggestion(
PropertyModel model, OmniboxSuggestion suggestion, SuggestionViewDelegate delegate) {
AnswerText[] details = AnswerTextClassic.from(mContext, suggestion);
SuggestionAnswer answer = suggestion.getAnswer();
if (answer != null) {
model.set(SuggestionViewProperties.HAS_ANSWER_IMAGE, answer.getSecondLine().hasImage());
}
model.set(SuggestionViewProperties.IS_ANSWER, true);
model.set(SuggestionViewProperties.DELEGATE, delegate);
model.set(SuggestionViewProperties.TEXT_LINE_1_SIZING,
Pair.create(TypedValue.COMPLEX_UNIT_SP, details[0].mHeightSp));
model.set(SuggestionViewProperties.TEXT_LINE_1_TEXT,
new SuggestionTextContainer(details[0].mText));
model.set(SuggestionViewProperties.TEXT_LINE_1_MAX_LINES, details[0].mMaxLines);
model.set(SuggestionViewProperties.TEXT_LINE_1_TEXT_DIRECTION, View.TEXT_DIRECTION_INHERIT);
if (details[1] != null) {
model.set(SuggestionViewProperties.TEXT_LINE_2_SIZING,
Pair.create(TypedValue.COMPLEX_UNIT_SP, details[1].mHeightSp));
model.set(SuggestionViewProperties.TEXT_LINE_2_TEXT,
new SuggestionTextContainer(details[1].mText));
model.set(SuggestionViewProperties.TEXT_LINE_2_MAX_LINES, details[1].mMaxLines);
model.set(SuggestionViewProperties.TEXT_LINE_2_TEXT_DIRECTION,
View.TEXT_DIRECTION_INHERIT);
}
model.set(SuggestionViewProperties.SUGGESTION_ICON_TYPE, SuggestionIcon.MAGNIFIER);
model.set(SuggestionViewProperties.REFINABLE, true);
}
/**
* Sets both lines of the Omnibox suggestion based on an Answers in Suggest result.
*/
private void setStateForNewSuggestion(
PropertyModel model, OmniboxSuggestion suggestion, SuggestionViewDelegate delegate) {
SuggestionAnswer answer = suggestion.getAnswer();
AnswerText[] details = AnswerTextNewLayout.from(
mContext, suggestion, mUrlBarEditingTextProvider.getTextWithAutocomplete());
model.set(AnswerSuggestionViewProperties.DELEGATE, delegate);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_1_SIZE, details[0].mHeightSp);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_2_SIZE, details[1].mHeightSp);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_1_TEXT, details[0].mText);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_2_TEXT, details[1].mText);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_1_MAX_LINES, details[0].mMaxLines);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_2_MAX_LINES, details[1].mMaxLines);
@AnswerIcon
int icon = AnswerIcon.UNDEFINED;
if (answer != null) {
switch (answer.getType()) {
case AnswerType.DICTIONARY:
icon = AnswerIcon.DICTIONARY;
break;
case AnswerType.FINANCE:
icon = AnswerIcon.FINANCE;
break;
case AnswerType.KNOWLEDGE_GRAPH:
icon = AnswerIcon.KNOWLEDGE;
break;
case AnswerType.SUNRISE:
icon = AnswerIcon.SUNRISE;
break;
case AnswerType.TRANSLATION:
icon = AnswerIcon.TRANSLATION;
break;
case AnswerType.WEATHER:
icon = AnswerIcon.WEATHER;
break;
case AnswerType.WHEN_IS:
icon = AnswerIcon.EVENT;
break;
case AnswerType.CURRENCY:
icon = AnswerIcon.CURRENCY;
break;
case AnswerType.SPORTS:
icon = AnswerIcon.SPORTS;
}
} else {
assert suggestion.getType() == OmniboxSuggestionType.CALCULATOR;
icon = AnswerIcon.CALCULATOR;
}
model.set(AnswerSuggestionViewProperties.ANSWER_ICON_TYPE, icon);
}
}