// Copyright 2017 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.searchwidget;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.ActivityOptionsCompat;
import org.jni_zero.CheckDiscard;
import org.chromium.base.Callback;
import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ThreadUtils;
import org.chromium.base.cached_flags.BooleanCachedFieldTrialParameter;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneShotCallback;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.app.tabmodel.TabWindowManagerSingleton;
import org.chromium.chrome.browser.back_press.BackPressManager;
import org.chromium.chrome.browser.browserservices.intents.WebappConstants;
import org.chromium.chrome.browser.content.WebContentsFactory;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.init.ActivityProfileProvider;
import org.chromium.chrome.browser.init.AsyncInitializationActivity;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.metrics.UmaActivityObserver;
import org.chromium.chrome.browser.omnibox.BackKeyBehaviorDelegate;
import org.chromium.chrome.browser.omnibox.LocationBarCoordinator;
import org.chromium.chrome.browser.omnibox.LocationBarEmbedderUiOverrides;
import org.chromium.chrome.browser.omnibox.UrlFocusChangeListener;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxLoadUrlParams;
import org.chromium.chrome.browser.omnibox.suggestions.action.OmniboxActionDelegateImpl;
import org.chromium.chrome.browser.password_manager.ManagePasswordsReferrer;
import org.chromium.chrome.browser.password_manager.PasswordManagerLauncher;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl;
import org.chromium.chrome.browser.profiles.OTRProfileID;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.rlz.RevenueStats;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabBuilder;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.toolbar.VoiceToolbarButtonController;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarManageable;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.IntentOrigin;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.SearchType;
import org.chromium.chrome.browser.ui.system.StatusBarColorController;
import org.chromium.components.browser_ui.modaldialog.AppModalPresenter;
import org.chromium.components.metrics.OmniboxEventProtos.OmniboxEventProto.PageClassification;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.base.ActivityKeyboardVisibilityDelegate;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.WindowDelegate;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.url.GURL;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
/** Queries the user's default search engine and shows autocomplete suggestions. */
public class SearchActivity extends AsyncInitializationActivity
implements SnackbarManageable, BackKeyBehaviorDelegate, UrlFocusChangeListener {
// Shared with other org.chromium.chrome.browser.searchwidget classes.
protected static final String TAG = "searchwidget";
public static final String EXTRA_FROM_SEARCH_ACTIVITY =
"org.chromium.chrome.browser.searchwidget.FROM_SEARCH_ACTIVITY";
@VisibleForTesting
/* package */ static final String USED_ANY_FROM_SEARCH_WIDGET = "SearchWidget.WidgetSelected";
@VisibleForTesting
/* package */ static final String USED_TEXT_FROM_SHORTCUTS_WIDGET =
"QuickActionSearchWidget.TextQuery";
@VisibleForTesting
/* package */ static final String USED_VOICE_FROM_SHORTCUTS_WIDGET =
"QuickActionSearchWidget.VoiceQuery";
@VisibleForTesting
/* package */ static final String USED_LENS_FROM_SHORTCUTS_WIDGET =
"QuickActionSearchWidget.LensQuery";
@VisibleForTesting
/* package */ static final String HISTOGRAM_LAUNCHED_WITH_QUERY =
"Android.Omnibox.SearchActivity.LaunchedWithQuery";
@VisibleForTesting
/* package */ static final String HISTOGRAM_INTENT_ORIGIN =
"Android.Omnibox.SearchActivity.IntentOrigin";
private static final String HISTOGRAM_REQUESTED_SEARCH_TYPE = //
"Android.Omnibox.SearchActivity.RequestedSearchType";
private static final String HISTOGRAM_INTENT_ACTIVITY_PRESENT =
"Android.Omnibox.SearchActivity.ActivityPresent";
@VisibleForTesting
/* package */ static final String HISTOGRAM_INTENT_REFERRER_VALID =
"Android.Omnibox.SearchActivity.ReferrerValid";
@VisibleForTesting
/* package */ static final String HISTOGRAM_NAVIGATION_TARGET_TYPE =
"Android.Omnibox.SearchActivity.NavigationTargetType";
@VisibleForTesting
/* package */ static final String HISTOGRAM_SESSION_TERMINATION_REASON =
"Android.Omnibox.SearchActivity.SessionTerminationReason";
/** Controls whether Referrer App ID is passed to Search Results Page via client= param. */
public static final BooleanCachedFieldTrialParameter SEARCH_IN_CCT_APPLY_REFERRER_ID =
ChromeFeatureList.newBooleanCachedFieldTrialParameter(
ChromeFeatureList.SEARCH_IN_CCT, "apply_referrer_id", false);
// NOTE: This is used to capture HISTOGRAM_NAVIGATION_TARGET_TYPE.
// Do not shuffle or reassign values.
@VisibleForTesting
@IntDef({
NavigationTargetType.URL,
NavigationTargetType.SEARCH,
NavigationTargetType.NATIVE_PAGE,
NavigationTargetType.COUNT
})
@Retention(RetentionPolicy.SOURCE)
@interface NavigationTargetType {
int URL = 0;
int SEARCH = 1;
int NATIVE_PAGE = 2;
int COUNT = 3;
}
// NOTE: This is used to capture HISTOGRAM_SESSION_TERMINATION_REASON.
// Do not shuffle or reassign values.
@IntDef({
TerminationReason.NAVIGATION,
TerminationReason.UNSPECIFIED,
TerminationReason.TAP_OUTSIDE,
TerminationReason.BACK_KEY_PRESSED,
TerminationReason.OMNIBOX_FOCUS_LOST,
TerminationReason.ACTIVITY_FOCUS_LOST,
TerminationReason.FRE_NOT_COMPLETED,
TerminationReason.COUNT
})
@Retention(RetentionPolicy.SOURCE)
@interface TerminationReason {
int NAVIGATION = 0;
int UNSPECIFIED = 1;
int TAP_OUTSIDE = 2;
int BACK_KEY_PRESSED = 3;
int OMNIBOX_FOCUS_LOST = 4;
int ACTIVITY_FOCUS_LOST = 5;
int FRE_NOT_COMPLETED = 6;
int COUNT = 7;
}
@VisibleForTesting /* package */ static final String CCT_CLIENT_PACKAGE_PREFIX = "app-cct-";
/** Notified about events happening inside a SearchActivity. */
public static class SearchActivityDelegate {
/**
* Called when {@link SearchActivity#triggerLayoutInflation} is deciding whether to continue
* loading the native library immediately.
*
* @return Whether or not native initialization should proceed immediately.
*/
boolean shouldDelayNativeInitialization() {
return false;
}
/**
* Called to launch the search engine dialog if it's needed.
*
* @param activity Activity that is launching the dialog.
* @param onSearchEngineFinalized Called when the dialog has been dismissed.
*/
void showSearchEngineDialogIfNeeded(
Activity activity, Callback<Boolean> onSearchEngineFinalized) {
LocaleManager.getInstance()
.showSearchEnginePromoIfNeeded(activity, onSearchEngineFinalized);
}
/** Called when {@link SearchActivity#finishDeferredInitialization} is done. */
void onFinishDeferredInitialization() {}
}
/** Notified about events happening for the SearchActivity. */
private static SearchActivityDelegate sDelegate;
// Incoming intent request type. See {@link SearchActivityUtils#IntentOrigin}.
@IntentOrigin Integer mIntentOrigin;
// Incoming intent search type. See {@link SearchActivityUtils#SearchType}.
@SearchType Integer mSearchType;
/** Whether the user is now allowed to perform searches. */
private boolean mIsActivityUsable;
/** Input submitted before before the native library was loaded. */
private OmniboxLoadUrlParams mQueuedParams;
private LocationBarCoordinator mLocationBarCoordinator;
private SearchActivityLocationBarLayout mSearchBox;
private SnackbarManager mSnackbarManager;
private Tab mTab;
private final ObservableSupplierImpl<Profile> mProfileSupplier = new ObservableSupplierImpl<>();
// SearchBoxDataProvider and LocationBarEmbedderUiOverrides are passed to several child
// components upon construction. Ensure we don't accidentally introduce disconnection by
// keeping only a single live instance here.
private final SearchBoxDataProvider mSearchBoxDataProvider = new SearchBoxDataProvider();
private final LocationBarEmbedderUiOverrides mLocationBarUiOverrides =
new LocationBarEmbedderUiOverrides();
private UmaActivityObserver mUmaActivityObserver;
public SearchActivity() {
mUmaActivityObserver = new UmaActivityObserver(this);
mLocationBarUiOverrides.setForcedPhoneStyleOmnibox();
}
@Override
protected boolean shouldDelayBrowserStartup() {
return true;
}
@Override
protected ActivityWindowAndroid createWindowAndroid() {
return new ActivityWindowAndroid(
this,
/* listenToActivityState= */ true,
new ActivityKeyboardVisibilityDelegate(new WeakReference(this)),
getIntentRequestTracker()) {
@Override
public ModalDialogManager getModalDialogManager() {
return SearchActivity.this.getModalDialogManager();
}
};
}
@Override
protected ModalDialogManager createModalDialogManager() {
return new ModalDialogManager(
new AppModalPresenter(this), ModalDialogManager.ModalDialogType.APP);
}
@Override
protected void triggerLayoutInflation() {
enableHardwareAcceleration();
mSnackbarManager = new SnackbarManager(this, findViewById(android.R.id.content), null);
mSearchBoxDataProvider.initialize(this);
ViewGroup rootView = (ViewGroup) getWindow().getDecorView().getRootView();
// Setting fitsSystemWindows to false ensures that the root view doesn't consume the
// insets.
rootView.setFitsSystemWindows(false);
var contentView = createContentView();
setContentView(contentView);
// Build the search box.
mSearchBox = contentView.findViewById(R.id.search_location_bar);
View anchorView = contentView.findViewById(R.id.toolbar);
// Update the status bar's color based on the toolbar color.
Drawable anchorViewBackground = anchorView.getBackground();
assert anchorViewBackground instanceof GradientDrawable
: "Unsupported background drawable.";
if (anchorViewBackground instanceof GradientDrawable) {
int anchorViewColor =
((GradientDrawable) anchorViewBackground).getColor().getDefaultColor();
StatusBarColorController.setStatusBarColor(this.getWindow(), anchorViewColor);
}
BackPressManager backPressManager = new BackPressManager();
getOnBackPressedDispatcher().addCallback(this, backPressManager.getCallback());
mLocationBarCoordinator =
new LocationBarCoordinator(
mSearchBox,
anchorView,
mProfileSupplier,
PrivacyPreferencesManagerImpl.getInstance(),
mSearchBoxDataProvider,
null,
new WindowDelegate(getWindow()),
getWindowAndroid(),
/* activityTabSupplier= */ () -> null,
getModalDialogManagerSupplier(),
/* shareDelegateSupplier= */ null,
/* incognitoStateProvider= */ null,
getLifecycleDispatcher(),
this::loadUrl,
/* backKeyBehavior= */ this,
/* pageInfoAction= */ (tab, pageInfoHighlight) -> {},
IntentHandler::bringTabToFront,
/* saveOfflineButtonState= */ (tab) -> false,
/*omniboxUma*/ (url, transition, isNtp) -> {},
TabWindowManagerSingleton::getInstance,
/* bookmarkState= */ (url) -> false,
VoiceToolbarButtonController::isToolbarMicEnabled,
/* merchantTrustSignalsCoordinatorSupplier= */ null,
new OmniboxActionDelegateImpl(
this,
() -> mSearchBoxDataProvider.getTab(),
// TODO(ender): phase out callbacks when the modules below are
// components.
// Open URL in an existing, else new regular tab.
url -> {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setComponent(
new ComponentName(
getApplicationContext(),
ChromeLauncherActivity.class));
intent.putExtra(
WebappConstants.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB,
true);
startActivity(intent);
},
// Open Incognito Tab callback:
() ->
startActivity(
IntentHandler.createTrustedOpenNewTabIntent(
this, true)),
// Open Password Settings callback:
() ->
PasswordManagerLauncher.showPasswordSettings(
this,
getProfileProviderSupplier()
.get()
.getOriginalProfile(),
ManagePasswordsReferrer.CHROME_SETTINGS,
() -> getModalDialogManager(),
/* managePasskeys= */ false),
// Open Quick Delete Dialog callback:
null),
null,
backPressManager,
/* OmniboxSuggestionsDropdownScrollListener= */ null,
/* tabModelSelectorSupplier= */ null,
mLocationBarUiOverrides,
null);
mLocationBarCoordinator.setUrlBarFocusable(true);
mLocationBarCoordinator.setShouldShowMicButtonWhenUnfocused(true);
mLocationBarCoordinator.getOmniboxStub().addUrlFocusChangeListener(this);
// Kick off everything needed for the user to type into the box.
handleNewIntent(getIntent(), false);
// Kick off loading of the native library.
if (!getActivityDelegate().shouldDelayNativeInitialization()) {
mHandler.post(this::startDelayedNativeInitialization);
}
onInitialLayoutInflationComplete();
}
/**
* Process newly received intent.
*
* @param intent the intent to be processed
* @param activityPresent whether activity was already showing when the intent was received
*/
@VisibleForTesting
/* package */ void handleNewIntent(Intent intent, boolean activityPresent) {
mIntentOrigin = SearchActivityUtils.getIntentOrigin(intent);
mSearchType = SearchActivityUtils.getIntentSearchType(intent);
RecordHistogram.recordEnumeratedHistogram(
HISTOGRAM_INTENT_ORIGIN, mIntentOrigin, IntentOrigin.COUNT);
RecordHistogram.recordEnumeratedHistogram(
HISTOGRAM_REQUESTED_SEARCH_TYPE, mSearchType, SearchType.COUNT);
RecordHistogram.recordBooleanHistogram(HISTOGRAM_INTENT_ACTIVITY_PRESENT, activityPresent);
recordUsage(mIntentOrigin, mSearchType);
switch (mIntentOrigin) {
case IntentOrigin.CUSTOM_TAB:
// Note: this may be refined by refinePageClassWithProfile().
mSearchBoxDataProvider.setPageClassification(PageClassification.OTHER_ON_CCT_VALUE);
mLocationBarUiOverrides
.setLensEntrypointAllowed(false)
.setVoiceEntrypointAllowed(false);
break;
case IntentOrigin.QUICK_ACTION_SEARCH_WIDGET:
mLocationBarUiOverrides
.setLensEntrypointAllowed(true)
.setVoiceEntrypointAllowed(true);
mSearchBoxDataProvider.setPageClassification(
PageClassification.ANDROID_SHORTCUTS_WIDGET_VALUE);
break;
case IntentOrigin.SEARCH_WIDGET:
// fallthrough
default:
mLocationBarUiOverrides
.setLensEntrypointAllowed(false)
.setVoiceEntrypointAllowed(true);
mSearchBoxDataProvider.setPageClassification(
PageClassification.ANDROID_SEARCH_WIDGET_VALUE);
break;
}
var profile = mProfileSupplier.get();
if (profile != null) refinePageClassWithProfile(profile);
mSearchBoxDataProvider.setCurrentUrl(SearchActivityUtils.getIntentUrl(intent));
beginQuery();
}
/** Translate current intent origin and extras to a PageClassification. */
@VisibleForTesting
/* package */ void refinePageClassWithProfile(@NonNull Profile profile) {
int pageClass = mSearchBoxDataProvider.getPageClassification(false);
// Verify if the PageClassification can be refined.
var url = SearchActivityUtils.getIntentUrl(getIntent());
if (pageClass != PageClassification.OTHER_ON_CCT_VALUE || GURL.isEmptyOrInvalid(url)) {
return;
}
var templateSvc = TemplateUrlServiceFactory.getForProfile(profile);
if (templateSvc != null && templateSvc.isSearchResultsPageFromDefaultSearchProvider(url)) {
mSearchBoxDataProvider.setPageClassification(
PageClassification.SEARCH_RESULT_PAGE_ON_CCT_VALUE);
} else {
mSearchBoxDataProvider.setPageClassification(PageClassification.OTHER_ON_CCT_VALUE);
}
}
@Override
protected OneshotSupplier<ProfileProvider> createProfileProvider() {
ActivityProfileProvider profileProvider =
new ActivityProfileProvider(getLifecycleDispatcher()) {
@Nullable
@Override
protected OTRProfileID createOffTheRecordProfileID() {
throw new IllegalStateException(
"Attempting to access incognito from the search activity");
}
};
profileProvider.onAvailable(
(provider) -> {
mProfileSupplier.set(profileProvider.get().getOriginalProfile());
});
return profileProvider;
}
@Override
public void finishNativeInitialization() {
super.finishNativeInitialization();
if (mProfileSupplier.hasValue()) {
finishNativeInitializationWithProfile(mProfileSupplier.get());
} else {
new OneShotCallback<>(
mProfileSupplier,
(profile) -> {
if (isDestroyed()) return;
finishNativeInitializationWithProfile(profile);
});
}
}
private void finishNativeInitializationWithProfile(Profile profile) {
refinePageClassWithProfile(profile);
WebContents webContents = WebContentsFactory.createWebContents(profile, false, false);
mTab =
new TabBuilder(profile)
.setWindow(getWindowAndroid())
.setLaunchType(TabLaunchType.FROM_EXTERNAL_APP)
.setWebContents(webContents)
.setDelegateFactory(new SearchActivityTabDelegateFactory())
.build();
mTab.loadUrl(new LoadUrlParams(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL));
mSearchBoxDataProvider.onNativeLibraryReady(mTab);
// Force the user to choose a search engine if they have to.
final Callback<Boolean> onSearchEngineFinalizedCallback =
(result) -> {
if (isActivityFinishingOrDestroyed()) return;
if (result == null || !result.booleanValue()) {
Log.e(TAG, "User failed to select a default search engine.");
finish(TerminationReason.FRE_NOT_COMPLETED);
return;
}
mHandler.post(this::finishDeferredInitialization);
};
getActivityDelegate()
.showSearchEngineDialogIfNeeded(
SearchActivity.this, onSearchEngineFinalizedCallback);
}
// OverrideBackKeyBehaviorDelegate implementation.
@Override
public boolean handleBackKeyPressed() {
finish(TerminationReason.BACK_KEY_PRESSED);
return true;
}
@VisibleForTesting
void finishDeferredInitialization() {
assert !mIsActivityUsable
: "finishDeferredInitialization() incorrectly called multiple times";
mIsActivityUsable = true;
if (mQueuedParams != null) {
// SearchActivity does not support incognito operation.
loadUrl(mQueuedParams, /* isIncognito= */ false);
}
// TODO(tedchoc): Warmup triggers the CustomTab layout to be inflated, but this widget
// will navigate to Tabbed mode. Investigate whether this can inflate
// the tabbed mode layout in the background instead of CCTs.
CustomTabsConnection.getInstance().warmup(0);
mSearchBox.onDeferredStartup(mSearchType, getWindowAndroid());
getActivityDelegate().onFinishDeferredInitialization();
}
@Override
protected View getViewToBeDrawnBeforeInitializingNative() {
return mSearchBox;
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleNewIntent(intent, true);
}
@Override
public void onPauseWithNative() {
umaSessionEnd();
RevenueStats.setCustomTabSearchClient(null);
super.onPauseWithNative();
}
@Override
public void onResumeWithNative() {
// Start a new UMA session for the new activity.
umaSessionResume();
if (mIntentOrigin == IntentOrigin.CUSTOM_TAB
&& SEARCH_IN_CCT_APPLY_REFERRER_ID.getValue()) {
var referrer = SearchActivityUtils.getReferrer(getIntent());
var referrerValid = !TextUtils.isEmpty(referrer);
RecordHistogram.recordBooleanHistogram(HISTOGRAM_INTENT_REFERRER_VALID, referrerValid);
RevenueStats.setCustomTabSearchClient(
referrerValid ? CCT_CLIENT_PACKAGE_PREFIX + referrer : null);
}
// Inform the actity lifecycle observers. Among other things, the observers record
// metrics pertaining to the "resumed" activity. This needs to happens after
// umaSessionResume has closed the old UMA record, pertaining to the previous
// (backgrounded) activity, and opened a new one pertaining to the "resumed" activity.
super.onResumeWithNative();
}
/** Initiate new UMA session, associating metrics with appropriate Activity type. */
private void umaSessionResume() {
mUmaActivityObserver.startUmaSession(
mIntentOrigin == IntentOrigin.CUSTOM_TAB
? ActivityType.CUSTOM_TAB
: ActivityType.TABBED,
null,
getWindowAndroid());
}
/** Mark that the UMA session has ended. */
private void umaSessionEnd() {
mUmaActivityObserver.endUmaSession();
}
@Override
public SnackbarManager getSnackbarManager() {
return mSnackbarManager;
}
private void beginQuery() {
var query = SearchActivityUtils.getIntentQuery(getIntent());
RecordHistogram.recordBooleanHistogram(
HISTOGRAM_LAUNCHED_WITH_QUERY, !TextUtils.isEmpty(query));
mSearchBox.beginQuery(
mIntentOrigin,
mSearchType,
SearchActivityUtils.getIntentQuery(getIntent()),
getWindowAndroid());
}
@Override
protected void onDestroy() {
if (mTab != null && mTab.isInitialized()) mTab.destroy();
if (mLocationBarCoordinator != null && mLocationBarCoordinator.getOmniboxStub() != null) {
mLocationBarCoordinator.getOmniboxStub().removeUrlFocusChangeListener(this);
mLocationBarCoordinator.destroy();
mLocationBarCoordinator = null;
}
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
@Override
public boolean shouldStartGpuProcess() {
return true;
}
@Override
public void onUrlFocusChange(boolean hasFocus) {
if (hasFocus) {
mLocationBarCoordinator.setUrlFocusChangeInProgress(false);
}
}
/* package */ boolean loadUrl(OmniboxLoadUrlParams params, boolean isIncognito) {
recordNavigationTargetType(new GURL(params.url));
if (mIntentOrigin == IntentOrigin.CUSTOM_TAB) {
SearchActivityUtils.resolveOmniboxRequestForResult(this, params);
} else {
loadUrlInChromeBrowser(params);
}
finish(TerminationReason.NAVIGATION);
return true;
}
private void loadUrlInChromeBrowser(@NonNull OmniboxLoadUrlParams params) {
if (!mIsActivityUsable) {
// Wait until native has loaded.
mQueuedParams = params;
return;
}
Intent intent = SearchActivityUtils.createIntentForStartActivity(this, params);
if (intent == null) return;
if (mIntentOrigin == IntentOrigin.SEARCH_WIDGET) {
intent.putExtra(SearchWidgetProvider.EXTRA_FROM_SEARCH_WIDGET, true);
}
IntentUtils.safeStartActivity(
this,
intent,
ActivityOptionsCompat.makeCustomAnimation(
this, android.R.anim.fade_in, android.R.anim.fade_out)
.toBundle());
RecordUserAction.record("SearchWidget.SearchMade");
LocaleManager.getInstance()
.recordLocaleBasedSearchMetrics(true, params.url, params.transitionType);
}
@VisibleForTesting
/* package */ ViewGroup createContentView() {
var contentView =
(ViewGroup) getLayoutInflater().inflate(R.layout.search_activity, null, false);
contentView.setOnClickListener(v -> finish(TerminationReason.TAP_OUTSIDE));
return contentView;
}
/**
* Terminate search session, invoking animations appropriate for the session type, and recording
* session termination reason.
*
* <p>This method should be called instead of {@link finish()}.
*
* @param reason the reason session was terminated
*/
@VisibleForTesting
/* package */ void finish(@TerminationReason int reason) {
if (isFinishing()) return;
var exitAnimationRes = 0;
if (mIntentOrigin != null && mIntentOrigin == IntentOrigin.CUSTOM_TAB) {
if (reason != TerminationReason.NAVIGATION) {
SearchActivityUtils.resolveOmniboxRequestForResult(this, null);
}
exitAnimationRes = android.R.anim.fade_out;
} else {
exitAnimationRes = R.anim.activity_close_exit;
}
recordEnumeratedHistogramWithIntentOriginBreakdown(
HISTOGRAM_SESSION_TERMINATION_REASON, reason, TerminationReason.COUNT);
super.finish();
overridePendingTransition(0, exitAnimationRes);
}
@Override
public void finish() {
finish(TerminationReason.UNSPECIFIED);
}
@VisibleForTesting
/* package */ static void recordUsage(@IntentOrigin int origin, @SearchType int searchType) {
var name =
switch (origin) {
case IntentOrigin.SEARCH_WIDGET -> USED_ANY_FROM_SEARCH_WIDGET;
case IntentOrigin.QUICK_ACTION_SEARCH_WIDGET -> switch (searchType) {
case SearchType.TEXT -> USED_TEXT_FROM_SHORTCUTS_WIDGET;
case SearchType.VOICE -> USED_VOICE_FROM_SHORTCUTS_WIDGET;
case SearchType.LENS -> USED_LENS_FROM_SHORTCUTS_WIDGET;
default -> null;
};
// Tracked by Custom Tabs.
case IntentOrigin.CUSTOM_TAB -> null;
default -> null;
};
if (name != null) RecordUserAction.record(name);
}
private static SearchActivityDelegate getActivityDelegate() {
ThreadUtils.checkUiThread();
if (sDelegate == null) sDelegate = new SearchActivityDelegate();
return sDelegate;
}
/** See {@link #sDelegate}. */
static void setDelegateForTests(SearchActivityDelegate delegate) {
var oldValue = sDelegate;
sDelegate = delegate;
ResettersForTesting.register(() -> sDelegate = oldValue);
}
@VisibleForTesting
void recordNavigationTargetType(@NonNull GURL url) {
var templateSvc = TemplateUrlServiceFactory.getForProfile(mProfileSupplier.get());
boolean isSearch =
templateSvc != null
&& templateSvc.isSearchResultsPageFromDefaultSearchProvider(url);
boolean isNative =
NativePage.isNativePageUrl(
url, /* incognito= */ false, /* hasPdfDownload= */ false);
int targetType =
isNative
? NavigationTargetType.NATIVE_PAGE
: isSearch ? NavigationTargetType.SEARCH : NavigationTargetType.URL;
recordEnumeratedHistogramWithIntentOriginBreakdown(
HISTOGRAM_NAVIGATION_TARGET_TYPE, targetType, NavigationTargetType.COUNT);
}
/**
* An extension of {@link RecordHistogram.recordEnumeratedHistogram} that captures the value
* with an additional breakdown by {@link IntentOrigin}.
*
* <p>The break down may not be captured if the histogram is recorded ahead of origin being
* identified (e.g. the activity was terminated before it was able to process the intent).
*
* @param histogramName the name of histogram to record
* @param sample the sample value to record
* @param max the maximum value the histogram can take
*/
private void recordEnumeratedHistogramWithIntentOriginBreakdown(
String histogramName, int sample, int max) {
RecordHistogram.recordEnumeratedHistogram(histogramName, sample, max);
if (mIntentOrigin != null) {
String suffix =
switch (mIntentOrigin) {
case IntentOrigin.CUSTOM_TAB -> ".CustomTab";
case IntentOrigin.QUICK_ACTION_SEARCH_WIDGET -> ".ShortcutsWidget";
default -> ".SearchWidget";
};
RecordHistogram.recordEnumeratedHistogram(histogramName + suffix, sample, max);
}
}
/* package */ void setLocationBarCoordinatorForTesting(LocationBarCoordinator coordinator) {
mLocationBarCoordinator = coordinator;
}
/* package */ LocationBarCoordinator getLocationBarCoordinatorForTesting() {
return mLocationBarCoordinator;
}
/* package */ void setActivityUsableForTesting(boolean isUsable) {
mIsActivityUsable = isUsable;
}
/* package */ SearchBoxDataProvider getSearchBoxDataProviderForTesting() {
return mSearchBoxDataProvider;
}
/* package */ LocationBarEmbedderUiOverrides getEmbedderUiOverridesForTesting() {
return mLocationBarUiOverrides;
}
/* package */ ObservableSupplier<Profile> getProfileSupplierForTesting() {
return mProfileSupplier;
}
/* package */ void setLocationBarLayoutForTesting(SearchActivityLocationBarLayout layout) {
mSearchBox = layout;
}
/* package */ void setUmaActivityObserverForTesting(UmaActivityObserver observer) {
mUmaActivityObserver = observer;
}
@Override
@SuppressWarnings("MissingSuperCall")
public void onTopResumedActivityChanged(boolean isTopResumedActivity) {
super_onTopResumedActivityChanged(isTopResumedActivity);
// TODO(crbug.com/329702834): Ensure showing Suggestions when activity resumes.
// This may only happen when user enters tab switcher, and immediately returns to the
// SearchActivity.
if (!isTopResumedActivity) {
mSearchBox.clearOmniboxFocus();
} else {
mSearchBox.requestOmniboxFocus();
}
}
@CheckDiscard("Isolated for testing; should be inlined by Proguard")
/* package */ void super_onTopResumedActivityChanged(boolean isTopResumedActivity) {
super.onTopResumedActivityChanged(isTopResumedActivity);
}
}