1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher; 18 19 import android.content.ActivityNotFoundException; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.PackageManager; 23 import android.content.pm.ResolveInfo; 24 import android.content.res.Configuration; 25 import android.graphics.drawable.Drawable; 26 import android.os.Bundle; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.KeyEvent; 30 import android.view.View; 31 import android.view.View.OnClickListener; 32 import android.view.View.OnKeyListener; 33 import android.view.View.OnLongClickListener; 34 import android.view.animation.AccelerateDecelerateInterpolator; 35 import android.view.animation.Animation; 36 import android.view.animation.Interpolator; 37 import android.view.animation.Transformation; 38 import android.view.inputmethod.InputMethodManager; 39 import android.widget.ImageButton; 40 import android.widget.LinearLayout; 41 import android.widget.TextView; 42 43 public class Search extends LinearLayout 44 implements OnClickListener, OnKeyListener, OnLongClickListener { 45 46 // Speed at which the widget slides up/down, in pixels/ms. 47 private static final float ANIMATION_VELOCITY = 1.0f; 48 49 /** The distance in dips between the optical top of the widget and the top if its bounds */ 50 private static final float WIDGET_TOP_OFFSET = 9; 51 52 private final String TAG = "SearchWidget"; 53 54 private Launcher mLauncher; 55 56 private TextView mSearchText; 57 private ImageButton mVoiceButton; 58 59 /** The animation that morphs the search widget to the search dialog. */ 60 private Animation mMorphAnimation; 61 62 /** The animation that morphs the search widget back to its normal position. */ 63 private Animation mUnmorphAnimation; 64 65 // These four are passed to Launcher.startSearch() when the search widget 66 // has finished morphing. They are instance variables to make it possible to update 67 // them while the widget is morphing. 68 private String mInitialQuery; 69 private boolean mSelectInitialQuery; 70 private Bundle mAppSearchData; 71 private boolean mGlobalSearch; 72 73 // For voice searching 74 private Intent mVoiceSearchIntent; 75 76 private int mWidgetTopOffset; 77 78 /** 79 * Used to inflate the Workspace from XML. 80 * 81 * @param context The application's context. 82 * @param attrs The attributes set containing the Workspace's customization values. 83 */ Search(Context context, AttributeSet attrs)84 public Search(Context context, AttributeSet attrs) { 85 super(context, attrs); 86 87 final float scale = context.getResources().getDisplayMetrics().density; 88 mWidgetTopOffset = Math.round(WIDGET_TOP_OFFSET * scale); 89 90 Interpolator interpolator = new AccelerateDecelerateInterpolator(); 91 92 mMorphAnimation = new ToParentOriginAnimation(); 93 // no need to apply transformation before the animation starts, 94 // since the gadget is already in its normal place. 95 mMorphAnimation.setFillBefore(false); 96 // stay in the top position after the animation finishes 97 mMorphAnimation.setFillAfter(true); 98 mMorphAnimation.setInterpolator(interpolator); 99 mMorphAnimation.setAnimationListener(new Animation.AnimationListener() { 100 // The amount of time before the animation ends to show the search dialog. 101 private static final long TIME_BEFORE_ANIMATION_END = 80; 102 103 // The runnable which we'll pass to our handler to show the search dialog. 104 private final Runnable mShowSearchDialogRunnable = new Runnable() { 105 public void run() { 106 showSearchDialog(); 107 } 108 }; 109 110 public void onAnimationEnd(Animation animation) { } 111 public void onAnimationRepeat(Animation animation) { } 112 public void onAnimationStart(Animation animation) { 113 // Make the search dialog show up ideally *just* as the animation reaches 114 // the top, to aid the illusion that the widget becomes the search dialog. 115 // Otherwise, there is a short delay when the widget reaches the top before 116 // the search dialog shows. We do this roughly 80ms before the animation ends. 117 getHandler().postDelayed( 118 mShowSearchDialogRunnable, 119 Math.max(mMorphAnimation.getDuration() - TIME_BEFORE_ANIMATION_END, 0)); 120 } 121 }); 122 123 mUnmorphAnimation = new FromParentOriginAnimation(); 124 // stay in the top position until the animation starts 125 mUnmorphAnimation.setFillBefore(true); 126 // no need to apply transformation after the animation finishes, 127 // since the gadget is now back in its normal place. 128 mUnmorphAnimation.setFillAfter(false); 129 mUnmorphAnimation.setInterpolator(interpolator); 130 mUnmorphAnimation.setAnimationListener(new Animation.AnimationListener(){ 131 public void onAnimationEnd(Animation animation) { 132 clearAnimation(); 133 } 134 public void onAnimationRepeat(Animation animation) { } 135 public void onAnimationStart(Animation animation) { } 136 }); 137 138 mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH); 139 mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL, 140 android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); 141 } 142 143 /** 144 * Implements OnClickListener. 145 */ onClick(View v)146 public void onClick(View v) { 147 if (v == mVoiceButton) { 148 startVoiceSearch(); 149 } else { 150 mLauncher.onSearchRequested(); 151 } 152 } 153 startVoiceSearch()154 private void startVoiceSearch() { 155 try { 156 getContext().startActivity(mVoiceSearchIntent); 157 } catch (ActivityNotFoundException ex) { 158 // Should not happen, since we check the availability of 159 // voice search before showing the button. But just in case... 160 Log.w(TAG, "Could not find voice search activity"); 161 } 162 } 163 164 /** 165 * Sets the query text. The query field is not editable, instead we forward 166 * the key events to the launcher, which keeps track of the text, 167 * calls setQuery() to show it, and gives it to the search dialog. 168 */ setQuery(String query)169 public void setQuery(String query) { 170 mSearchText.setText(query, TextView.BufferType.NORMAL); 171 } 172 173 /** 174 * Morph the search gadget to the search dialog. 175 * See {@link Activity#startSearch()} for the arguments. 176 */ startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch)177 public void startSearch(String initialQuery, boolean selectInitialQuery, 178 Bundle appSearchData, boolean globalSearch) { 179 mInitialQuery = initialQuery; 180 mSelectInitialQuery = selectInitialQuery; 181 mAppSearchData = appSearchData; 182 mGlobalSearch = globalSearch; 183 184 if (isAtTop()) { 185 showSearchDialog(); 186 } else { 187 // Call up the keyboard before we actually call the search dialog so that it 188 // (hopefully) animates in at about the same time as the widget animation, and 189 // so that it becomes available as soon as possible. Only do this if a hard 190 // keyboard is not currently available. 191 if (getContext().getResources().getConfiguration().hardKeyboardHidden == 192 Configuration.HARDKEYBOARDHIDDEN_YES) { 193 InputMethodManager inputManager = (InputMethodManager) 194 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 195 inputManager.showSoftInputUnchecked(0, null); 196 } 197 198 // Start the animation, unless it has already started. 199 if (getAnimation() != mMorphAnimation) { 200 mMorphAnimation.setDuration(getAnimationDuration()); 201 startAnimation(mMorphAnimation); 202 } 203 } 204 } 205 206 /** 207 * Shows the system search dialog immediately, without any animation. 208 */ showSearchDialog()209 private void showSearchDialog() { 210 mLauncher.showSearchDialog( 211 mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch); 212 } 213 214 /** 215 * Restore the search gadget to its normal position. 216 * 217 * @param animate Whether to animate the movement of the gadget. 218 */ stopSearch(boolean animate)219 public void stopSearch(boolean animate) { 220 setQuery(""); 221 222 // Only restore if we are not already restored. 223 if (getAnimation() == mMorphAnimation) { 224 if (animate && !isAtTop()) { 225 mUnmorphAnimation.setDuration(getAnimationDuration()); 226 startAnimation(mUnmorphAnimation); 227 } else { 228 clearAnimation(); 229 } 230 } 231 } 232 isAtTop()233 private boolean isAtTop() { 234 return getWidgetTop() == 0; 235 } 236 getAnimationDuration()237 private int getAnimationDuration() { 238 return (int) (getWidgetTop() / ANIMATION_VELOCITY); 239 } 240 241 /** 242 * Modify clearAnimation() to invalidate the parent. This works around 243 * an issue where the region where the end of the animation placed the view 244 * was not redrawn after clearing the animation. 245 */ 246 @Override clearAnimation()247 public void clearAnimation() { 248 Animation animation = getAnimation(); 249 if (animation != null) { 250 super.clearAnimation(); 251 if (animation.hasEnded() 252 && animation.getFillAfter() 253 && animation.willChangeBounds()) { 254 ((View) getParent()).invalidate(); 255 } else { 256 invalidate(); 257 } 258 } 259 } 260 onKey(View v, int keyCode, KeyEvent event)261 public boolean onKey(View v, int keyCode, KeyEvent event) { 262 if (!event.isSystem() && 263 (keyCode != KeyEvent.KEYCODE_DPAD_UP) && 264 (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) && 265 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && 266 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && 267 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { 268 // Forward key events to Launcher, which will forward text 269 // to search dialog 270 switch (event.getAction()) { 271 case KeyEvent.ACTION_DOWN: 272 return mLauncher.onKeyDown(keyCode, event); 273 case KeyEvent.ACTION_MULTIPLE: 274 return mLauncher.onKeyMultiple(keyCode, event.getRepeatCount(), event); 275 case KeyEvent.ACTION_UP: 276 return mLauncher.onKeyUp(keyCode, event); 277 } 278 } 279 return false; 280 } 281 282 /** 283 * Implements OnLongClickListener to pass long clicks on child views 284 * to the widget. This makes it possible to pick up the widget by long 285 * clicking on the text field or a button. 286 */ onLongClick(View v)287 public boolean onLongClick(View v) { 288 return performLongClick(); 289 } 290 291 @Override onFinishInflate()292 protected void onFinishInflate() { 293 super.onFinishInflate(); 294 295 mSearchText = (TextView) findViewById(R.id.search_src_text); 296 mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn); 297 298 mSearchText.setOnKeyListener(this); 299 300 mSearchText.setOnClickListener(this); 301 mVoiceButton.setOnClickListener(this); 302 setOnClickListener(this); 303 304 mSearchText.setOnLongClickListener(this); 305 mVoiceButton.setOnLongClickListener(this); 306 307 // Set the placeholder text to be the Google logo within the search widget. 308 Drawable googlePlaceholder = 309 getContext().getResources().getDrawable(R.drawable.placeholder_google); 310 mSearchText.setCompoundDrawablesWithIntrinsicBounds(googlePlaceholder, null, null, null); 311 312 configureVoiceSearchButton(); 313 } 314 315 @Override onDetachedFromWindow()316 public void onDetachedFromWindow() { 317 super.onDetachedFromWindow(); 318 } 319 320 /** 321 * If appropriate & available, configure voice search 322 * 323 * Note: Because the home screen search widget is always web search, we only check for 324 * getVoiceSearchLaunchWebSearch() modes. We don't support the alternate form of app-specific 325 * voice search. 326 */ configureVoiceSearchButton()327 private void configureVoiceSearchButton() { 328 // Enable the voice search button if there is an activity that can handle it 329 PackageManager pm = getContext().getPackageManager(); 330 ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent, 331 PackageManager.MATCH_DEFAULT_ONLY); 332 boolean voiceSearchVisible = ri != null; 333 334 // finally, set visible state of voice search button, as appropriate 335 mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE); 336 } 337 338 /** 339 * Sets the {@link Launcher} that this gadget will call on to display the search dialog. 340 */ setLauncher(Launcher launcher)341 public void setLauncher(Launcher launcher) { 342 mLauncher = launcher; 343 } 344 345 /** 346 * Moves the view to the top left corner of its parent. 347 */ 348 private class ToParentOriginAnimation extends Animation { 349 @Override applyTransformation(float interpolatedTime, Transformation t)350 protected void applyTransformation(float interpolatedTime, Transformation t) { 351 float dx = -getLeft() * interpolatedTime; 352 float dy = -getWidgetTop() * interpolatedTime; 353 t.getMatrix().setTranslate(dx, dy); 354 } 355 } 356 357 /** 358 * Moves the view from the top left corner of its parent. 359 */ 360 private class FromParentOriginAnimation extends Animation { 361 @Override applyTransformation(float interpolatedTime, Transformation t)362 protected void applyTransformation(float interpolatedTime, Transformation t) { 363 float dx = -getLeft() * (1.0f - interpolatedTime); 364 float dy = -getWidgetTop() * (1.0f - interpolatedTime); 365 t.getMatrix().setTranslate(dx, dy); 366 } 367 } 368 369 /** 370 * The widget is centered vertically within it's 4x1 slot. This is 371 * accomplished by nesting the actual widget inside another view. For 372 * animation purposes, we care about the top of the actual widget rather 373 * than it's container. This method return the top of the actual widget. 374 */ getWidgetTop()375 private int getWidgetTop() { 376 return getTop() + getChildAt(0).getTop() + mWidgetTopOffset; 377 } 378 379 } 380