| /* |
| * 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.launcher2; |
| |
| 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 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); |
| |
| mSearchText.setOnKeyListener(this); |
| |
| mSearchText.setOnClickListener(this); |
| mVoiceButton.setOnClickListener(this); |
| setOnClickListener(this); |
| |
| mSearchText.setOnLongClickListener(this); |
| mVoiceButton.setOnLongClickListener(this); |
| |
| // Set the placeholder text to be the Google logo within the search widget. |
| Drawable googlePlaceholder = |
| getContext().getResources().getDrawable(R.drawable.placeholder_google); |
| mSearchText.setCompoundDrawablesWithIntrinsicBounds(googlePlaceholder, null, null, null); |
| |
| configureVoiceSearchButton(); |
| } |
| |
| @Override |
| public void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| |
| } |