blob: 9b303b569eee4065c6b91ddacabfed2842e14115 [file] [log] [blame]
// Copyright 2022 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.base;
import android.content.Context;
import android.content.Intent;
import android.util.SparseBooleanArray;
import androidx.annotation.NonNull;
import androidx.collection.ArraySet;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.chrome.browser.omnibox.suggestions.SuggestionHost;
import org.chromium.components.browser_ui.widget.chips.ChipProperties;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.EntityInfoProto;
import org.chromium.components.omnibox.OmniboxMetrics;
import org.chromium.components.omnibox.action.OmniboxAction;
import org.chromium.components.omnibox.action.OmniboxActionDelegate;
import org.chromium.components.omnibox.action.OmniboxActionInSuggest;
import org.chromium.components.omnibox.action.OmniboxActionType;
import org.chromium.components.omnibox.action.OmniboxPedal;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.Set;
/**
* A class that handles model creation for the Action Chips.
*/
public class ActionChipsProcessor {
private final @NonNull Context mContext;
private final @NonNull OmniboxActionDelegate mOmniboxActionDelegate;
private final @NonNull SuggestionHost mSuggestionHost;
private final @NonNull Set<Integer> mLastVisiblePedals = new ArraySet<>();
private final @NonNull SparseBooleanArray mActionInSuggestShownOrUsed =
new SparseBooleanArray();
private final boolean mDialerAvailable;
private int mJourneysActionShownPosition = -1;
/**
* @param context An Android context.
* @param suggestionHost Component receiving suggestion events.
* @param omniboxActionDelegate A delegate that will responsible for pedals.
*/
public ActionChipsProcessor(@NonNull Context context, @NonNull SuggestionHost suggestionHost,
@NonNull OmniboxActionDelegate omniboxActionDelegate) {
mContext = context;
mSuggestionHost = suggestionHost;
mOmniboxActionDelegate = omniboxActionDelegate;
// TODO(crbug/1418077): Migrate this to OmniboxActionInSuggest along with execute logic.
var pm = mContext.getPackageManager();
var dialIntent = new Intent(Intent.ACTION_DIAL);
mDialerAvailable = !pm.queryIntentActivities(dialIntent, 0).isEmpty();
}
public void onUrlFocusChange(boolean hasFocus) {
if (!hasFocus) {
recordActionsShown();
}
}
public void onSuggestionsReceived() {
mActionInSuggestShownOrUsed.clear();
}
/**
* Setup ActionChips for the suggestion.
*
* @param suggestion The suggestion to process.
* @param model Property model to update.
* @param position The position of the suggestion with OmniboxAction(s) on the suggestion list.
*/
public void populateModel(AutocompleteMatch suggestion, PropertyModel model, int position) {
if (!doesProcessSuggestion(suggestion, position)) {
model.set(ActionChipsProperties.ACTION_CHIPS, null);
return;
}
var actionChipList = suggestion.getActions();
var modelList = new ModelList();
// The header item increases lead-in padding before the first actual chip is shown.
// In default state, the chips will align with the suggestion text, but when scrolled
// the chips may show up under the decoration.
modelList.add(new ListItem(ActionChipsProperties.ViewType.HEADER, new PropertyModel()));
for (OmniboxAction chip : actionChipList) {
if (!actionSupported(chip)) continue;
final var chipModel =
new PropertyModel.Builder(ChipProperties.ALL_KEYS)
.with(ChipProperties.TEXT, chip.hint)
.with(ChipProperties.CONTENT_DESCRIPTION,
mContext.getString(
R.string.accessibility_omnibox_pedal, chip.hint))
.with(ChipProperties.ENABLED, true)
.with(ChipProperties.CLICK_HANDLER, m -> executeAction(chip, position))
.with(ChipProperties.ICON, chip.icon.iconRes)
.with(ChipProperties.APPLY_ICON_TINT, chip.icon.tintWithTextColor)
.build();
modelList.add(new ListItem(ActionChipsProperties.ViewType.CHIP, chipModel));
// TODO(crbug/1418077): Move this to appropriate implementations.
switch (chip.actionId) {
case OmniboxActionType.PEDAL:
mLastVisiblePedals.add(OmniboxPedal.from(chip).pedalId);
break;
case OmniboxActionType.HISTORY_CLUSTERS:
mJourneysActionShownPosition = position;
break;
case OmniboxActionType.ACTION_IN_SUGGEST:
var actionType = OmniboxActionInSuggest.from(chip)
.actionInfo.getActionType()
.getNumber();
mActionInSuggestShownOrUsed.put(actionType, false);
break;
}
}
model.set(ActionChipsProperties.ACTION_CHIPS, modelList);
}
private boolean doesProcessSuggestion(AutocompleteMatch suggestion, int position) {
// TODO(crbug/1418077): Migrate this to OmniboxActionInSuggest along with execute logic.
for (int index = 0; index < suggestion.getActions().size(); ++index) {
if (actionSupported(suggestion.getActions().get(index))) return true;
}
return false;
}
/**
* Evaluates whether a given action is supported.
* TODO(crbug/1418077): Migrate this to OmniboxActionInSuggest along with execute logic.
*/
private boolean actionSupported(@NonNull OmniboxAction action) {
switch (action.actionId) {
case OmniboxActionType.PEDAL:
case OmniboxActionType.HISTORY_CLUSTERS:
return true;
case OmniboxActionType.ACTION_IN_SUGGEST:
return OmniboxActionInSuggest.from(action).actionInfo.getActionType().getNumber()
!= EntityInfoProto.ActionInfo.ActionType.CALL_VALUE
|| mDialerAvailable;
}
return false;
}
/**
* Invoke action associated with the ActionChip.
*
* TODO(crbug/1418077): Move this to appropriate implementations.
*/
private void executeAction(@NonNull OmniboxAction action, int position) {
switch (action.actionId) {
case OmniboxActionType.HISTORY_CLUSTERS:
OmniboxMetrics.recordResumeJourneyClick(position);
break;
case OmniboxActionType.ACTION_IN_SUGGEST:
var actionType =
OmniboxActionInSuggest.from(action).actionInfo.getActionType().getNumber();
mActionInSuggestShownOrUsed.put(actionType, true);
break;
}
mSuggestionHost.finishInteraction();
mOmniboxActionDelegate.execute(action);
}
/**
* Record the actions shown for all action types (Journeys + any pedals).
*
* TODO(crbug/1418077): Move this to appropriate implementations.
*/
private void recordActionsShown() {
for (Integer pedal : mLastVisiblePedals) {
OmniboxMetrics.recordPedalShown(pedal);
}
for (var actionIndex = 0; actionIndex < mActionInSuggestShownOrUsed.size(); actionIndex++) {
int actionType = mActionInSuggestShownOrUsed.keyAt(actionIndex);
boolean wasUsed = mActionInSuggestShownOrUsed.valueAt(actionIndex);
OmniboxMetrics.recordActionInSuggestShown(actionType);
if (wasUsed) {
OmniboxMetrics.recordActionInSuggestUsed(actionType);
}
}
OmniboxMetrics.recordResumeJourneyShown(mJourneysActionShownPosition);
mJourneysActionShownPosition = -1;
mLastVisiblePedals.clear();
}
}