summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher2/Search.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher2/Search.java')
-rw-r--r--src/com/android/launcher2/Search.java407
1 files changed, 407 insertions, 0 deletions
diff --git a/src/com/android/launcher2/Search.java b/src/com/android/launcher2/Search.java
new file mode 100644
index 0000000..8a7c352
--- /dev/null
+++ b/src/com/android/launcher2/Search.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher;
+
+import android.app.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.server.search.SearchableInfo;
+import android.server.search.Searchables;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.View.OnLongClickListener;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class Search extends LinearLayout
+ implements OnClickListener, OnKeyListener, OnLongClickListener {
+
+ // Speed at which the widget slides up/down, in pixels/ms.
+ private static final float ANIMATION_VELOCITY = 1.0f;
+
+ private final String TAG = "SearchWidget";
+
+ private Launcher mLauncher;
+
+ private TextView mSearchText;
+ private ImageButton mVoiceButton;
+
+ /** The animation that morphs the search widget to the search dialog. */
+ private Animation mMorphAnimation;
+
+ /** The animation that morphs the search widget back to its normal position. */
+ private Animation mUnmorphAnimation;
+
+ // These four are passed to Launcher.startSearch() when the search widget
+ // has finished morphing. They are instance variables to make it possible to update
+ // them while the widget is morphing.
+ private String mInitialQuery;
+ private boolean mSelectInitialQuery;
+ private Bundle mAppSearchData;
+ private boolean mGlobalSearch;
+
+ // For voice searching
+ private Intent mVoiceSearchIntent;
+
+ private Drawable mGooglePlaceholder;
+
+ private SearchManager mSearchManager;
+
+ /**
+ * Used to inflate the Workspace from XML.
+ *
+ * @param context The application's context.
+ * @param attrs The attributes set containing the Workspace's customization values.
+ */
+ public Search(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Interpolator interpolator = new AccelerateDecelerateInterpolator();
+
+ mMorphAnimation = new ToParentOriginAnimation();
+ // no need to apply transformation before the animation starts,
+ // since the gadget is already in its normal place.
+ mMorphAnimation.setFillBefore(false);
+ // stay in the top position after the animation finishes
+ mMorphAnimation.setFillAfter(true);
+ mMorphAnimation.setInterpolator(interpolator);
+ mMorphAnimation.setAnimationListener(new Animation.AnimationListener() {
+ // The amount of time before the animation ends to show the search dialog.
+ private static final long TIME_BEFORE_ANIMATION_END = 80;
+
+ // The runnable which we'll pass to our handler to show the search dialog.
+ private final Runnable mShowSearchDialogRunnable = new Runnable() {
+ public void run() {
+ showSearchDialog();
+ }
+ };
+
+ public void onAnimationEnd(Animation animation) { }
+ public void onAnimationRepeat(Animation animation) { }
+ public void onAnimationStart(Animation animation) {
+ // Make the search dialog show up ideally *just* as the animation reaches
+ // the top, to aid the illusion that the widget becomes the search dialog.
+ // Otherwise, there is a short delay when the widget reaches the top before
+ // the search dialog shows. We do this roughly 80ms before the animation ends.
+ getHandler().postDelayed(
+ mShowSearchDialogRunnable,
+ Math.max(mMorphAnimation.getDuration() - TIME_BEFORE_ANIMATION_END, 0));
+ }
+ });
+
+ mUnmorphAnimation = new FromParentOriginAnimation();
+ // stay in the top position until the animation starts
+ mUnmorphAnimation.setFillBefore(true);
+ // no need to apply transformation after the animation finishes,
+ // since the gadget is now back in its normal place.
+ mUnmorphAnimation.setFillAfter(false);
+ mUnmorphAnimation.setInterpolator(interpolator);
+ mUnmorphAnimation.setAnimationListener(new Animation.AnimationListener(){
+ public void onAnimationEnd(Animation animation) {
+ clearAnimation();
+ }
+ public void onAnimationRepeat(Animation animation) { }
+ public void onAnimationStart(Animation animation) { }
+ });
+
+ mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
+ mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+
+ mSearchManager = (SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE);
+ }
+
+ /**
+ * Implements OnClickListener.
+ */
+ public void onClick(View v) {
+ if (v == mVoiceButton) {
+ startVoiceSearch();
+ } else {
+ mLauncher.onSearchRequested();
+ }
+ }
+
+ private void startVoiceSearch() {
+ try {
+ getContext().startActivity(mVoiceSearchIntent);
+ } catch (ActivityNotFoundException ex) {
+ // Should not happen, since we check the availability of
+ // voice search before showing the button. But just in case...
+ Log.w(TAG, "Could not find voice search activity");
+ }
+ }
+
+ /**
+ * Sets the query text. The query field is not editable, instead we forward
+ * the key events to the launcher, which keeps track of the text,
+ * calls setQuery() to show it, and gives it to the search dialog.
+ */
+ public void setQuery(String query) {
+ mSearchText.setText(query, TextView.BufferType.NORMAL);
+ }
+
+ /**
+ * Morph the search gadget to the search dialog.
+ * See {@link Activity.startSearch()} for the arguments.
+ */
+ public void startSearch(String initialQuery, boolean selectInitialQuery,
+ Bundle appSearchData, boolean globalSearch) {
+ mInitialQuery = initialQuery;
+ mSelectInitialQuery = selectInitialQuery;
+ mAppSearchData = appSearchData;
+ mGlobalSearch = globalSearch;
+
+ if (isAtTop()) {
+ showSearchDialog();
+ } else {
+ // Call up the keyboard before we actually call the search dialog so that it
+ // (hopefully) animates in at about the same time as the widget animation, and
+ // so that it becomes available as soon as possible. Only do this if a hard
+ // keyboard is not currently available.
+ if (getContext().getResources().getConfiguration().hardKeyboardHidden ==
+ Configuration.HARDKEYBOARDHIDDEN_YES) {
+ InputMethodManager inputManager = (InputMethodManager)
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputManager.showSoftInputUnchecked(0, null);
+ }
+
+ // Start the animation, unless it has already started.
+ if (getAnimation() != mMorphAnimation) {
+ mMorphAnimation.setDuration(getAnimationDuration());
+ startAnimation(mMorphAnimation);
+ }
+ }
+ }
+
+ /**
+ * Shows the system search dialog immediately, without any animation.
+ */
+ private void showSearchDialog() {
+ mLauncher.showSearchDialog(
+ mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch);
+ }
+
+ /**
+ * Restore the search gadget to its normal position.
+ *
+ * @param animate Whether to animate the movement of the gadget.
+ */
+ public void stopSearch(boolean animate) {
+ setQuery("");
+
+ // Only restore if we are not already restored.
+ if (getAnimation() == mMorphAnimation) {
+ if (animate && !isAtTop()) {
+ mUnmorphAnimation.setDuration(getAnimationDuration());
+ startAnimation(mUnmorphAnimation);
+ } else {
+ clearAnimation();
+ }
+ }
+ }
+
+ private boolean isAtTop() {
+ return getTop() == 0;
+ }
+
+ private int getAnimationDuration() {
+ return (int) (getTop() / ANIMATION_VELOCITY);
+ }
+
+ /**
+ * Modify clearAnimation() to invalidate the parent. This works around
+ * an issue where the region where the end of the animation placed the view
+ * was not redrawn after clearing the animation.
+ */
+ @Override
+ public void clearAnimation() {
+ Animation animation = getAnimation();
+ if (animation != null) {
+ super.clearAnimation();
+ if (animation.hasEnded()
+ && animation.getFillAfter()
+ && animation.willChangeBounds()) {
+ ((View) getParent()).invalidate();
+ } else {
+ invalidate();
+ }
+ }
+ }
+
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (!event.isSystem() &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
+ // Forward key events to Launcher, which will forward text
+ // to search dialog
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ return mLauncher.onKeyDown(keyCode, event);
+ case KeyEvent.ACTION_MULTIPLE:
+ return mLauncher.onKeyMultiple(keyCode, event.getRepeatCount(), event);
+ case KeyEvent.ACTION_UP:
+ return mLauncher.onKeyUp(keyCode, event);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Implements OnLongClickListener to pass long clicks on child views
+ * to the widget. This makes it possible to pick up the widget by long
+ * clicking on the text field or a button.
+ */
+ public boolean onLongClick(View v) {
+ return performLongClick();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mSearchText = (TextView) findViewById(R.id.search_src_text);
+ mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn);
+
+ mGooglePlaceholder = getContext().getResources().getDrawable(R.drawable.placeholder_google);
+ mContext.registerReceiver(mBroadcastReceiver,
+ new IntentFilter(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED));
+
+ mSearchText.setOnKeyListener(this);
+
+ mSearchText.setOnClickListener(this);
+ mVoiceButton.setOnClickListener(this);
+ setOnClickListener(this);
+
+ mSearchText.setOnLongClickListener(this);
+ mVoiceButton.setOnLongClickListener(this);
+
+ configureVoiceSearchButton();
+ setUpTextField();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mBroadcastReceiver != null) getContext().unregisterReceiver(mBroadcastReceiver);
+ }
+
+ /**
+ * If appropriate & available, configure voice search
+ *
+ * Note: Because the home screen search widget is always web search, we only check for
+ * getVoiceSearchLaunchWebSearch() modes. We don't support the alternate form of app-specific
+ * voice search.
+ */
+ private void configureVoiceSearchButton() {
+ // Enable the voice search button if there is an activity that can handle it
+ PackageManager pm = getContext().getPackageManager();
+ ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ boolean voiceSearchVisible = ri != null;
+
+ // finally, set visible state of voice search button, as appropriate
+ mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Sets up the look of the text field. If Google is the chosen search provider, includes
+ * a Google logo as placeholder.
+ */
+ private void setUpTextField() {
+ boolean showGooglePlaceholder = false;
+ SearchableInfo webSearchSearchable = mSearchManager.getDefaultSearchableForWebSearch();
+ if (webSearchSearchable != null) {
+ ComponentName webSearchComponent = webSearchSearchable.getSearchActivity();
+ if (webSearchComponent != null) {
+ String componentString = webSearchComponent.flattenToShortString();
+ if (Searchables.ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME.equals(componentString) ||
+ Searchables.GOOGLE_SEARCH_COMPONENT_NAME.equals(componentString)) {
+ showGooglePlaceholder = true;
+ }
+ }
+ }
+
+ mSearchText.setCompoundDrawablesWithIntrinsicBounds(
+ showGooglePlaceholder ? mGooglePlaceholder : null, null, null, null);
+ }
+
+ /**
+ * Sets the {@link Launcher} that this gadget will call on to display the search dialog.
+ */
+ public void setLauncher(Launcher launcher) {
+ mLauncher = launcher;
+ }
+
+ // Broadcast receiver for web search provider change notifications
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED.equals(action)) {
+ setUpTextField();
+ }
+ }
+ };
+
+ /**
+ * Moves the view to the top left corner of its parent.
+ */
+ private class ToParentOriginAnimation extends Animation {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float dx = -getLeft() * interpolatedTime;
+ float dy = -getTop() * interpolatedTime;
+ t.getMatrix().setTranslate(dx, dy);
+ }
+ }
+
+ /**
+ * Moves the view from the top left corner of its parent.
+ */
+ private class FromParentOriginAnimation extends Animation {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float dx = -getLeft() * (1.0f - interpolatedTime);
+ float dy = -getTop() * (1.0f - interpolatedTime);
+ t.getMatrix().setTranslate(dx, dy);
+ }
+ }
+
+}