| Index: chrome/android/java/src/org/chromium/chrome/browser/searchwidget/SearchActivity.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/searchwidget/SearchActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/searchwidget/SearchActivity.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..0c0d30d10bf94fbdc9e5a01cd8ad918b8bb01659
|
| --- /dev/null
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/searchwidget/SearchActivity.java
|
| @@ -0,0 +1,497 @@
|
| +// Copyright 2017 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.searchwidget;
|
| +
|
| +import android.animation.Animator;
|
| +import android.animation.AnimatorListenerAdapter;
|
| +import android.animation.ValueAnimator;
|
| +import android.content.Intent;
|
| +import android.graphics.Rect;
|
| +import android.graphics.drawable.Drawable;
|
| +import android.net.Uri;
|
| +import android.os.Handler;
|
| +import android.support.customtabs.CustomTabsIntent;
|
| +import android.support.v4.app.ActivityOptionsCompat;
|
| +import android.text.TextUtils;
|
| +import android.view.Gravity;
|
| +import android.view.View;
|
| +import android.view.View.MeasureSpec;
|
| +import android.view.ViewGroup;
|
| +import android.view.ViewGroup.LayoutParams;
|
| +import android.view.ViewStub;
|
| +import android.widget.FrameLayout;
|
| +
|
| +import org.chromium.base.ApiCompatibilityUtils;
|
| +import org.chromium.base.ContextUtils;
|
| +import org.chromium.chrome.R;
|
| +import org.chromium.chrome.browser.IntentHandler;
|
| +import org.chromium.chrome.browser.WebContentsFactory;
|
| +import org.chromium.chrome.browser.WindowDelegate;
|
| +import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
|
| +import org.chromium.chrome.browser.document.ChromeLauncherActivity;
|
| +import org.chromium.chrome.browser.init.AsyncInitializationActivity;
|
| +import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
|
| +import org.chromium.chrome.browser.omnibox.AutocompleteController;
|
| +import org.chromium.chrome.browser.omnibox.UrlBar;
|
| +import org.chromium.chrome.browser.profiles.Profile;
|
| +import org.chromium.chrome.browser.snackbar.SnackbarManager;
|
| +import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarManageable;
|
| +import org.chromium.chrome.browser.tab.Tab;
|
| +import org.chromium.chrome.browser.tab.TabDelegateFactory;
|
| +import org.chromium.chrome.browser.tab.TabIdManager;
|
| +import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType;
|
| +import org.chromium.chrome.browser.util.IntentUtils;
|
| +import org.chromium.chrome.browser.widget.FadingBackgroundView;
|
| +import org.chromium.components.url_formatter.UrlFormatter;
|
| +import org.chromium.content_public.browser.LoadUrlParams;
|
| +import org.chromium.ui.UiUtils;
|
| +import org.chromium.ui.base.ActivityWindowAndroid;
|
| +
|
| +/** Prototype that queries the user's default search engine and shows autocomplete suggestions. */
|
| +public class SearchActivity extends AsyncInitializationActivity
|
| + implements SnackbarManageable, SearchLocationBarLayout.Delegate,
|
| + View.OnLayoutChangeListener {
|
| + private static final String TAG = "searchwidget";
|
| +
|
| + /** How long the animation should run for. */
|
| + private static final int ANIMATION_DURATION_MS = 200;
|
| +
|
| + /** Padding gleaned from the background Drawable of the search box. */
|
| + private final Rect mSearchBoxPadding = new Rect();
|
| +
|
| + /** Location of the search box on the home screen. */
|
| + private Rect mSearchBoxWidgetBounds;
|
| +
|
| + /** Small margin/padding used for the search box. */
|
| + private int mSpacingSmall;
|
| +
|
| + /** Medium margin/padding used for the search box. */
|
| + private int mSpacingMedium;
|
| +
|
| + /** Large margin/padding used for the search box. */
|
| + private int mSpacingLarge;
|
| +
|
| + /**
|
| + * View that the omnibox suggestion list anchors to. This is different from the main search box
|
| + * because the upstream LocationBarLayout code expects a full white box instead of a floating
|
| + * box with shadows.
|
| + */
|
| + private View mAnchorView;
|
| +
|
| + /** See {@link BoxAnimatorScrim}. */
|
| + private BoxAnimatorScrim mScrimView;
|
| +
|
| + /** Main content view. */
|
| + private ViewGroup mContentView;
|
| +
|
| + /** Whether or not the library has begun loading. */
|
| + private boolean mIsNativeLoading;
|
| +
|
| + /** Whether the native library has been loaded. */
|
| + private boolean mIsNativeReady;
|
| +
|
| + /** Input submitted before before the native library was loaded. */
|
| + private String mQueuedUrl;
|
| +
|
| + /** Animates the SearchActivity's controls moving into place. */
|
| + private ValueAnimator mAnimator;
|
| +
|
| + /** The View that represents the search box. */
|
| + private SearchLocationBarLayout mSearchBox;
|
| +
|
| + private UrlBar mUrlBar;
|
| +
|
| + private SearchBoxDataProvider mSearchBoxDataProvider;
|
| +
|
| + private SnackbarManager mSnackbarManager;
|
| + private ActivityWindowAndroid mWindowAndroid;
|
| + private Tab mTab;
|
| +
|
| + @Override
|
| + public void backKeyPressed() {
|
| + finish();
|
| + }
|
| +
|
| + @Override
|
| + public void onStop() {
|
| + finish();
|
| + super.onStop();
|
| + }
|
| +
|
| + @Override
|
| + public void finish() {
|
| + if (SearchWidgetProvider.ANIMATE_TRANSITION) {
|
| + // Update the search widgets so that they all show up as opaque again.
|
| + SearchWidgetProvider.setLaunchingWidgetId(SearchWidgetProvider.INVALID_WIDGET_ID);
|
| + SearchWidgetProvider.updateAllWidgets();
|
| + }
|
| +
|
| + super.finish();
|
| + overridePendingTransition(0, android.R.anim.fade_out);
|
| + }
|
| +
|
| + @Override
|
| + protected boolean shouldDelayBrowserStartup() {
|
| + return true;
|
| + }
|
| +
|
| + @Override
|
| + public boolean onActivityResultWithNative(int requestCode, int resultCode, Intent intent) {
|
| + if (super.onActivityResultWithNative(requestCode, resultCode, intent)) return true;
|
| + boolean result = mWindowAndroid.onActivityResult(requestCode, resultCode, intent);
|
| +
|
| + // The voice query should have completed. If the voice recognition isn't confident about
|
| + // what it heard, it puts the query into the omnibox. We need to focus it at that point.
|
| + if (mUrlBar != null) focusTextBox(false);
|
| +
|
| + return result;
|
| + }
|
| +
|
| + @Override
|
| + protected void setContentView() {
|
| + initializeDimensions();
|
| +
|
| + mWindowAndroid = new ActivityWindowAndroid(this);
|
| + mSnackbarManager = new SnackbarManager(this);
|
| +
|
| + // Build the Views that {@link SearchLocationBarLayout} expects to exist.
|
| + ViewStub resultsStub = new ViewStub(this);
|
| + resultsStub.setId(R.id.omnibox_results_container_stub);
|
| + resultsStub.setLayoutResource(R.layout.omnibox_results_container);
|
| +
|
| + // The FadingBackgroundView isn't used here, but is still animated by the LocationBarLayout.
|
| + // This interferes with the animation we need to show the widget moving to the right place.
|
| + FadingBackgroundView fadingView = new FadingBackgroundView(this, null) {
|
| + @Override
|
| + public void showFadingOverlay() {}
|
| + @Override
|
| + public void hideFadingOverlay(boolean fadeOut) {}
|
| + };
|
| +
|
| + fadingView.setId(R.id.fading_focus_target);
|
| + FrameLayout.LayoutParams fadingBackgroundLayoutParams = new FrameLayout.LayoutParams(0, 0);
|
| +
|
| + // Add an empty view to prevent crashing.
|
| + ViewGroup bottomContainer = new FrameLayout(this);
|
| + bottomContainer.setId(R.id.bottom_container);
|
| +
|
| + // Build the search box, set it to invisible until the animation is done.
|
| + mSearchBox = new SearchLocationBarLayout(this, null);
|
| + mSearchBox.setDelegate(this);
|
| + mSearchBox.setVisibility(mSearchBoxWidgetBounds == null ? View.VISIBLE : View.INVISIBLE);
|
| + mSearchBox.setBackgroundResource(R.drawable.card_single);
|
| + mSearchBox.setPadding(mSpacingLarge, mSpacingMedium, mSpacingLarge, mSpacingMedium);
|
| + mSearchBox.initializeControls(new WindowDelegate(getWindow()), mWindowAndroid);
|
| + mSearchBox.setUrlBarFocusable(true);
|
| + mSearchBoxDataProvider = new SearchBoxDataProvider();
|
| + mSearchBox.setToolbarDataProvider(mSearchBoxDataProvider);
|
| + FrameLayout.LayoutParams searchParams =
|
| + new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
| + ApiCompatibilityUtils.setMarginStart(searchParams, mSpacingSmall);
|
| + ApiCompatibilityUtils.setMarginEnd(searchParams, mSpacingSmall);
|
| + searchParams.topMargin = mSpacingSmall;
|
| +
|
| + // The scrim animates the search box moving into place.
|
| + mScrimView = new BoxAnimatorScrim(this, null);
|
| + mScrimView.setOnClickListener(new View.OnClickListener() {
|
| + @Override
|
| + public void onClick(View v) {
|
| + // Don't allow clicking on the scrim until the animation completes.
|
| + if (mScrimView.getInterpolatedValue() < 1.0f) return;
|
| +
|
| + // Finish the Activity if the user clicks on the scrim.
|
| + finish();
|
| + }
|
| + });
|
| + if (!SearchWidgetProvider.ANIMATE_TRANSITION) mScrimView.setInterpolatedValue(1.0f);
|
| +
|
| + // The anchor view puts the omnibox suggestions in the correct place, visually.
|
| + mAnchorView = new View(this);
|
| + mAnchorView.setId(R.id.toolbar);
|
| + mAnchorView.setClickable(true);
|
| + FrameLayout.LayoutParams anchorParams =
|
| + new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
| + anchorParams.gravity = Gravity.CENTER_HORIZONTAL;
|
| +
|
| + // Initialize and build the View hierarchy.
|
| + mContentView = createContentView(mSearchBox);
|
| + mContentView.addView(fadingView, fadingBackgroundLayoutParams);
|
| + mContentView.addView(mScrimView);
|
| + mContentView.addView(resultsStub);
|
| + mContentView.addView(mAnchorView, anchorParams);
|
| + mContentView.addView(mSearchBox, searchParams);
|
| + mContentView.addView(bottomContainer);
|
| + setContentView(mContentView);
|
| + mUrlBar = (UrlBar) mSearchBox.findViewById(R.id.url_bar);
|
| +
|
| + mSearchBox.setShowCachedZeroSuggestResults(true);
|
| + }
|
| +
|
| + @Override
|
| + public void finishNativeInitialization() {
|
| + super.finishNativeInitialization();
|
| + mIsNativeReady = true;
|
| +
|
| + mTab = new Tab(TabIdManager.getInstance().generateValidId(Tab.INVALID_TAB_ID),
|
| + Tab.INVALID_TAB_ID, false, this, mWindowAndroid, TabLaunchType.FROM_EXTERNAL_APP,
|
| + null, null);
|
| + mTab.initialize(WebContentsFactory.createWebContents(false, false), null,
|
| + new TabDelegateFactory(), false, false);
|
| + mTab.loadUrl(new LoadUrlParams("about:blank"));
|
| + mSearchBoxDataProvider.onNativeLibraryReady(mTab);
|
| + mSearchBox.onNativeLibraryReady();
|
| + mSearchBox.setAutocompleteProfile(Profile.getLastUsedProfile().getOriginalProfile());
|
| +
|
| + if (mQueuedUrl != null) loadUrl(mQueuedUrl);
|
| +
|
| + new Handler().post(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + onDeferredStartup();
|
| + }
|
| + });
|
| + }
|
| +
|
| + @Override
|
| + public void onDeferredStartup() {
|
| + super.onDeferredStartup();
|
| +
|
| + AutocompleteController.nativePrefetchZeroSuggestResults();
|
| + CustomTabsConnection.getInstance(getApplication()).warmup(0);
|
| +
|
| + if (!isVoiceSearchIntent() && mUrlBar.isFocused()) mSearchBox.onUrlFocusChange(true);
|
| + }
|
| +
|
| + @Override
|
| + protected View getViewToBeDrawnBeforeInitializingNative() {
|
| + return mSearchBox;
|
| + }
|
| +
|
| + @Override
|
| + public void onNewIntent(Intent intent) {
|
| + super.onNewIntent(intent);
|
| + setIntent(intent);
|
| + beginQuery();
|
| + }
|
| +
|
| + @Override
|
| + public SnackbarManager getSnackbarManager() {
|
| + return mSnackbarManager;
|
| + }
|
| +
|
| + private boolean isVoiceSearchIntent() {
|
| + return IntentUtils.safeGetBooleanExtra(
|
| + getIntent(), SearchWidgetProvider.EXTRA_START_VOICE_SEARCH, false);
|
| + }
|
| +
|
| + private void beginQuery() {
|
| + if (isVoiceSearchIntent()) {
|
| + mSearchBox.startVoiceRecognition();
|
| + } else {
|
| + focusTextBox(true);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void onDestroy() {
|
| + if (mTab != null && mTab.isInitialized()) mTab.destroy();
|
| + super.onDestroy();
|
| + }
|
| +
|
| + private void focusTextBox(boolean clearQuery) {
|
| + if (mIsNativeReady) mSearchBox.onUrlFocusChange(true);
|
| + mUrlBar.setCursorVisible(true);
|
| + mUrlBar.setIgnoreTextChangesForAutocomplete(true);
|
| + if (clearQuery) mUrlBar.setUrl("", null);
|
| + mUrlBar.setIgnoreTextChangesForAutocomplete(false);
|
| + mUrlBar.setSelection(0, mUrlBar.getText().length());
|
| + new Handler().post(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + UiUtils.showKeyboard(mUrlBar);
|
| + }
|
| + });
|
| + }
|
| +
|
| + @Override
|
| + public void loadUrl(String url) {
|
| + // Wait until native has loaded.
|
| + if (!mIsNativeReady) {
|
| + mQueuedUrl = url;
|
| + return;
|
| + }
|
| +
|
| + // Don't do anything if the input was empty. This is done after the native check to prevent
|
| + // resending a queued query after the user deleted it.
|
| + if (TextUtils.isEmpty(url)) return;
|
| +
|
| + // Fix up the URL and send it to a tab.
|
| + String fixedUrl = UrlFormatter.fixupUrl(url);
|
| + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(fixedUrl));
|
| + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
| +
|
| + boolean useHerbTab = ContextUtils.getAppSharedPreferences().getBoolean(
|
| + SearchWidgetProvider.PREF_USE_HERB_TAB, false);
|
| + if (useHerbTab) {
|
| + intent = ChromeLauncherActivity.createCustomTabActivityIntent(this, intent, true);
|
| + intent.putExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE,
|
| + CustomTabsIntent.SHOW_PAGE_TITLE);
|
| + } else {
|
| + intent.setPackage(getPackageName());
|
| + IntentHandler.addTrustedIntentExtras(intent);
|
| + }
|
| +
|
| + // TODO(dfalcantara): Make IntentUtils take in an ActivityOptions bundle once public.
|
| + startActivity(intent,
|
| + ActivityOptionsCompat
|
| + .makeCustomAnimation(this, android.R.anim.fade_in, android.R.anim.fade_out)
|
| + .toBundle());
|
| + finish();
|
| + }
|
| +
|
| + private ViewGroup createContentView(final View searchBox) {
|
| + assert mContentView == null;
|
| +
|
| + ViewGroup contentView = new FrameLayout(this) {
|
| + @Override
|
| + public void onMeasure(int widthSpec, int heightSpec) {
|
| + super.onMeasure(widthSpec, heightSpec);
|
| + if (mAnchorView == null) return;
|
| +
|
| + // Calculate how big the box is without shadow-induced padding.
|
| + FrameLayout.LayoutParams anchorParams =
|
| + (FrameLayout.LayoutParams) mAnchorView.getLayoutParams();
|
| + int anchorViewWidth = searchBox.getMeasuredWidth() - mSearchBoxPadding.left
|
| + - mSearchBoxPadding.right;
|
| + int anchorViewHeight =
|
| + mSpacingSmall + mSearchBox.getMeasuredHeight() - mSearchBoxPadding.bottom;
|
| + if (anchorParams.width == anchorViewWidth
|
| + && anchorParams.height == anchorViewHeight) {
|
| + return;
|
| + }
|
| +
|
| + // Move the anchor view up a little bit as a dirty hack until we can add a
|
| + // dimension. This allows the suggestion list to move up past the rounded corners of
|
| + // the search box.
|
| + anchorParams.topMargin = -(mSpacingSmall / 4);
|
| + anchorParams.width = anchorViewWidth;
|
| + anchorParams.height = anchorViewHeight;
|
| +
|
| + // Measure the anchor view to match the search box's height without its side or
|
| + // bottom shadow padding. This isn't exactly what we need, but it's hard to get the
|
| + // correct behavior because LocationBarLayout overrides how the suggestions are
|
| + // laid out.
|
| + int anchorWidthSpec =
|
| + MeasureSpec.makeMeasureSpec(anchorViewWidth, MeasureSpec.EXACTLY);
|
| + int anchorHeightSpec =
|
| + MeasureSpec.makeMeasureSpec(anchorViewHeight, MeasureSpec.EXACTLY);
|
| + measureChild(mAnchorView, anchorWidthSpec, anchorHeightSpec);
|
| + }
|
| + };
|
| +
|
| + contentView.addOnLayoutChangeListener(this);
|
| + contentView.setId(R.id.control_container);
|
| + return contentView;
|
| + }
|
| +
|
| + @Override
|
| + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
|
| + int oldTop, int oldRight, int oldBottom) {
|
| + if (mSearchBoxWidgetBounds != null) {
|
| + if (mAnimator == null) initializeAnimation();
|
| + } else {
|
| + // If there's no animation, then we can load the library immediately without worrying
|
| + // about jank.
|
| + beginLoadingLibrary();
|
| + }
|
| + mContentView.removeOnLayoutChangeListener(this);
|
| + }
|
| +
|
| + private void initializeDimensions() {
|
| + mSearchBoxWidgetBounds = getIntent().getSourceBounds();
|
| +
|
| + // Cache the padding of the Drawable that is used as the background for the search box.
|
| + Drawable searchBackground =
|
| + ApiCompatibilityUtils.getDrawable(getResources(), R.drawable.card_single);
|
| + searchBackground.getPadding(mSearchBoxPadding);
|
| +
|
| + // TODO(dfalcantara): Add values to the XML files instead of reusing random ones.
|
| + mSpacingSmall = getResources().getDimensionPixelSize(
|
| + R.dimen.tablet_toolbar_start_padding_no_buttons);
|
| + mSpacingMedium =
|
| + getResources().getDimensionPixelSize(R.dimen.location_bar_incognito_badge_padding);
|
| + mSpacingLarge =
|
| + getResources().getDimensionPixelSize(R.dimen.contextual_search_peek_promo_padding);
|
| + }
|
| +
|
| + private void initializeAnimation() {
|
| + assert SearchWidgetProvider.ANIMATE_TRANSITION;
|
| +
|
| + // The bounds of the home screen widget are given in window space coordinates, so they need
|
| + // to be converted into conrdinates that are relative from the root View. The converted
|
| + // bounds are used to animate the box moving from its widget location to the location at the
|
| + // top of the screen.
|
| + int[] rootWindowLocation = new int[2];
|
| + mContentView.getLocationInWindow(rootWindowLocation);
|
| +
|
| + final Rect sourceRect = new Rect();
|
| + sourceRect.left = mSearchBoxWidgetBounds.left - rootWindowLocation[0];
|
| + sourceRect.right = mSearchBoxWidgetBounds.right - rootWindowLocation[0];
|
| + sourceRect.top = mSearchBoxWidgetBounds.top - rootWindowLocation[1];
|
| + sourceRect.bottom = mSearchBoxWidgetBounds.bottom - rootWindowLocation[1];
|
| +
|
| + int[] targetBoxLocation = new int[2];
|
| + mSearchBox.getLocationInWindow(targetBoxLocation);
|
| + final Rect targetRect = new Rect();
|
| + targetRect.left = targetBoxLocation[0] - rootWindowLocation[0];
|
| + targetRect.right = targetRect.left + mSearchBox.getMeasuredWidth();
|
| + targetRect.top = targetBoxLocation[1] - rootWindowLocation[1];
|
| + targetRect.bottom = targetRect.top + mSearchBox.getMeasuredHeight();
|
| +
|
| + mScrimView.setAnimationRects(sourceRect, targetRect);
|
| +
|
| + mAnimator = ValueAnimator.ofFloat(0, 1);
|
| + mAnimator.setDuration(ANIMATION_DURATION_MS);
|
| + mAnimator.addListener(new AnimatorListenerAdapter() {
|
| + @Override
|
| + public void onAnimationStart(Animator animation) {
|
| + // Make the widget on the homescreen hide itself.
|
| + SearchWidgetProvider.updateAllWidgets();
|
| + }
|
| +
|
| + @Override
|
| + public void onAnimationEnd(Animator animation) {
|
| + // Defer loading the library until the box is in the right place to prevent jank.
|
| + beginLoadingLibrary();
|
| + }
|
| + });
|
| + mAnimator.addUpdateListener(mScrimView);
|
| +
|
| + new Handler().post(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + mAnimator.start();
|
| + }
|
| + });
|
| + }
|
| +
|
| + private void beginLoadingLibrary() {
|
| + if (mIsNativeLoading) return;
|
| + mIsNativeLoading = true;
|
| +
|
| + // Show the real search box and let the user type in it.
|
| + mScrimView.setInterpolatedValue(1.0f);
|
| + mSearchBox.setVisibility(View.VISIBLE);
|
| + beginQuery();
|
| + mHandler.post(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + mSearchBox.showCachedZeroSuggestResultsIfAvailable();
|
| + }
|
| + });
|
| + ChromeBrowserInitializer.getInstance(getApplicationContext())
|
| + .handlePreNativeStartup(SearchActivity.this);
|
| + }
|
| +}
|
|
|