diff options
Diffstat (limited to 'src/com/android/launcher2/Search.java')
-rw-r--r-- | src/com/android/launcher2/Search.java | 407 |
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); + } + } + +} |