/* * 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.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.os.Bundle; 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; /** * 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); } /** * 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; // 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) { // Make sure the text field is not focusable, so it's not responsible for // causing the whole view to shift up to accommodate the keyboard. mSearchText.setFocusable(false); InputMethodManager inputManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.showSoftInputUnchecked(0, null); } if (isAtTop()) { showSearchDialog(); } else { // 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(""); // Set the search field back to focusable after making it unfocusable in // startSearch, so that the home screen doesn't try to shift around when the // keyboard comes up. mSearchText.setFocusable(true); // 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); mSearchText.setOnKeyListener(this); mSearchText.setOnClickListener(this); mVoiceButton.setOnClickListener(this); setOnClickListener(this); mSearchText.setOnLongClickListener(this); mVoiceButton.setOnLongClickListener(this); configureVoiceSearchButton(); } /** * 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 the {@link Launcher} that this gadget will call on to display the search dialog. */ public void setLauncher(Launcher launcher) { mLauncher = launcher; } /** * 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); } } }