blob: 3e035083adbb56085c7a1f981be28ccf8d5abaca [file] [log] [blame]
// Copyright 2019 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.answer;
import android.content.Context;
import android.graphics.Bitmap;
import androidx.annotation.DrawableRes;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.LocaleUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.chrome.browser.omnibox.UrlBarEditingTextStateProvider;
import org.chromium.chrome.browser.omnibox.suggestions.SuggestionHost;
import org.chromium.chrome.browser.omnibox.suggestions.base.BaseSuggestionViewProcessor;
import org.chromium.chrome.browser.omnibox.suggestions.base.SuggestionDrawableState;
import org.chromium.components.image_fetcher.ImageFetcher;
import org.chromium.components.omnibox.AnswerType;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.OmniboxSuggestionType;
import org.chromium.components.omnibox.SuggestionAnswer;
import org.chromium.components.omnibox.action.OmniboxActionDelegate;
import org.chromium.components.omnibox.suggestions.OmniboxSuggestionUiType;
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 extends BaseSuggestionViewProcessor {
private static final String COLOR_REVERSAL_COUNTRY_LIST = "ja-JP,ko-KR,zh-CN,zh-TW";
private final Map<String, List<PropertyModel>> mPendingAnswerRequestUrls;
private final SuggestionHost mSuggestionHost;
private final UrlBarEditingTextStateProvider mUrlBarEditingTextProvider;
private final Supplier<ImageFetcher> mImageFetcherSupplier;
private boolean mOmniBoxAnswerColorReversal;
/**
* @param context An Android context.
* @param suggestionHost A handle to the object using the suggestions.
*/
public AnswerSuggestionProcessor(Context context, SuggestionHost suggestionHost,
OmniboxActionDelegate omniboxActionDelegate,
UrlBarEditingTextStateProvider editingTextProvider,
Supplier<ImageFetcher> imageFetcherSupplier) {
super(context, suggestionHost, omniboxActionDelegate, null);
mSuggestionHost = suggestionHost;
mPendingAnswerRequestUrls = new HashMap<>();
mUrlBarEditingTextProvider = editingTextProvider;
mImageFetcherSupplier = imageFetcherSupplier;
}
/**
* Evaluates whether the current locale uses "green" or "red" color to indicate
* growth, allowing locale-adjusted representation of stock market changes.
*/
@Override
public void onNativeInitialized() {
super.onNativeInitialized();
mOmniBoxAnswerColorReversal =
ChromeFeatureList.isEnabled(ChromeFeatureList.SUGGESTION_ANSWERS_COLOR_REVERSE);
}
@Override
public boolean doesProcessSuggestion(AutocompleteMatch suggestion, int position) {
// 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 int getViewTypeId() {
return OmniboxSuggestionUiType.ANSWER_SUGGESTION;
}
@Override
public PropertyModel createModel() {
return new PropertyModel(AnswerSuggestionViewProperties.ALL_KEYS);
}
@Override
public void populateModel(AutocompleteMatch suggestion, PropertyModel model, int position) {
super.populateModel(suggestion, model, position);
setStateForSuggestion(model, suggestion, position);
}
private void maybeFetchAnswerIcon(PropertyModel model, AutocompleteMatch suggestion) {
ThreadUtils.assertOnUiThread();
// Ensure an image fetcher is available prior to requesting images.
ImageFetcher imageFetcher = mImageFetcherSupplier.get();
if (imageFetcher == 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);
ImageFetcher.Params params =
ImageFetcher.Params.create(url, ImageFetcher.ANSWER_SUGGESTIONS_UMA_CLIENT_NAME);
imageFetcher.fetchImage(
params, (Bitmap bitmap) -> {
ThreadUtils.assertOnUiThread();
// Remove models for the URL ahead of all the checks to ensure we
// do not keep them around waiting in case image fetch failed.
List<PropertyModel> currentModels = mPendingAnswerRequestUrls.remove(url);
if (currentModels == null || bitmap == null) return;
for (int i = 0; i < currentModels.size(); i++) {
PropertyModel currentModel = currentModels.get(i);
setSuggestionDrawableState(currentModel,
SuggestionDrawableState.Builder.forBitmap(getContext(), bitmap)
.setLarge(true)
.build());
}
});
}
/**
* Sets both lines of the Omnibox suggestion based on an Answers in Suggest result.
*/
private void setStateForSuggestion(
PropertyModel model, AutocompleteMatch suggestion, int position) {
@AnswerType
int answerType = suggestion.getAnswer() == null ? AnswerType.INVALID
: suggestion.getAnswer().getType();
boolean suggestionTextColorReversal = checkColorReversalRequired(answerType);
AnswerText[] details = AnswerTextNewLayout.from(getContext(), suggestion,
mUrlBarEditingTextProvider.getTextWithoutAutocomplete(),
suggestionTextColorReversal);
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_ACCESSIBILITY_DESCRIPTION,
details[0].mAccessibilityDescription);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_2_ACCESSIBILITY_DESCRIPTION,
details[1].mAccessibilityDescription);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_1_MAX_LINES, details[0].mMaxLines);
model.set(AnswerSuggestionViewProperties.TEXT_LINE_2_MAX_LINES, details[1].mMaxLines);
setSuggestionDrawableState(model,
SuggestionDrawableState.Builder
.forDrawableRes(getContext(), getSuggestionIcon(suggestion))
.setLarge(true)
.build());
setTabSwitchOrRefineAction(model, suggestion, position);
maybeFetchAnswerIcon(model, suggestion);
}
/**
* Checks if we need to apply color reversion on the answer suggestion.
* @param answerType The type of a suggested answer.
*/
@VisibleForTesting
public boolean checkColorReversalRequired(@AnswerType int answerType) {
boolean isFinanceAnswer = answerType == AnswerType.FINANCE;
// Flag disabled.
if (!mOmniBoxAnswerColorReversal) return false;
// Country not eligible.
if (!isCountryEligibleForColorReversal()) return false;
// Not a finance answer.
if (!isFinanceAnswer) return false;
// All other cases.
return true;
}
/**
* Returns whether a given country is eligible for Answer color reversal.
* Note: this call does not verify the flag state.
*/
@VisibleForTesting
/* package */ boolean isCountryEligibleForColorReversal() {
return COLOR_REVERSAL_COUNTRY_LIST.contains(LocaleUtils.getDefaultLocaleString());
}
/**
* Get default suggestion icon for supplied suggestion.
*/
@DrawableRes
int getSuggestionIcon(AutocompleteMatch suggestion) {
SuggestionAnswer answer = suggestion.getAnswer();
if (answer != null) {
switch (answer.getType()) {
case AnswerType.DICTIONARY:
return R.drawable.ic_book_round;
case AnswerType.FINANCE:
return R.drawable.ic_swap_vert_round;
case AnswerType.KNOWLEDGE_GRAPH:
return R.drawable.ic_google_round;
case AnswerType.SUNRISE:
return R.drawable.ic_wb_sunny_round;
case AnswerType.TRANSLATION:
return R.drawable.logo_translate_round;
case AnswerType.WEATHER:
return R.drawable.logo_partly_cloudy;
case AnswerType.WHEN_IS:
return R.drawable.ic_event_round;
case AnswerType.CURRENCY:
return R.drawable.ic_loop_round;
case AnswerType.SPORTS:
return R.drawable.ic_google_round;
default:
break;
}
} else if (suggestion.getType() == OmniboxSuggestionType.CALCULATOR) {
return R.drawable.ic_equals_sign_round;
}
return R.drawable.ic_google_round;
}
}