diff options
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/launcher/Launcher.java | 218 | ||||
-rw-r--r-- | src/com/android/launcher/Search.java | 823 | ||||
-rw-r--r-- | src/com/android/launcher/Workspace.java | 102 |
3 files changed, 362 insertions, 781 deletions
diff --git a/src/com/android/launcher/Launcher.java b/src/com/android/launcher/Launcher.java index 7dd341819..3bf96aae8 100644 --- a/src/com/android/launcher/Launcher.java +++ b/src/com/android/launcher/Launcher.java @@ -20,6 +20,7 @@ import android.app.Activity; import android.app.AlertDialog; import android.app.Application; import android.app.Dialog; +import android.app.IWallpaperService; import android.app.SearchManager; import android.app.StatusBarManager; import android.content.ActivityNotFoundException; @@ -35,26 +36,24 @@ import android.content.Intent.ShortcutIconResource; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; import android.content.res.Configuration; +import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.Message; import android.os.MessageQueue; import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.Message; -import android.provider.*; -import android.telephony.PhoneNumberUtils; +import android.provider.LiveFolders; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -71,11 +70,11 @@ import android.view.ViewGroup; import android.view.View.OnLongClickListener; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; import android.widget.GridView; +import android.widget.ListView; import android.widget.SlidingDrawer; -import android.app.IWallpaperService; +import android.widget.TextView; +import android.widget.Toast; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; @@ -168,7 +167,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On private final BroadcastReceiver mApplicationsReceiver = new ApplicationsIntentReceiver(); private final ContentObserver mObserver = new FavoritesChangeObserver(); - private final ContentObserver mAppWidgetResetObserver = new AppWidgetResetObserver(); private LayoutInflater mInflater; @@ -353,6 +351,9 @@ public final class Launcher extends Activity implements View.OnClickListener, On if (mRestoring) { startLoaders(); } + + // Make sure that the search gadget (if any) is in its normal place. + stopSearch(false); } @Override @@ -387,45 +388,29 @@ public final class Launcher extends Activity implements View.OnClickListener, On boolean gotKey = TextKeyListener.getInstance().onKeyDown(mWorkspace, mDefaultKeySsb, keyCode, event); if (gotKey && mDefaultKeySsb != null && mDefaultKeySsb.length() > 0) { - // something usable has been typed - dispatch it now. - final String str = mDefaultKeySsb.toString(); - - boolean isDialable = true; - final int count = str.length(); - for (int i = 0; i < count; i++) { - if (!PhoneNumberUtils.isReallyDialable(str.charAt(i))) { - isDialable = false; - break; - } - } - Intent intent; - if (isDialable) { - intent = new Intent(Intent.ACTION_DIAL, Uri.fromParts("tel", str, null)); - } else { - intent = new Intent(Contacts.Intents.UI.FILTER_CONTACTS_ACTION); - intent.putExtra(Contacts.Intents.UI.FILTER_TEXT_EXTRA_KEY, str); - } - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - - try { - startActivity(intent); - } catch (android.content.ActivityNotFoundException ex) { - // Oh well... no one knows how to filter/dial. Life goes on. - } - - mDefaultKeySsb.clear(); - mDefaultKeySsb.clearSpans(); - Selection.setSelection(mDefaultKeySsb, 0); - - return true; + // something usable has been typed - start a search + // the typed text will be retrieved and cleared by + // showSearchDialog() + // If there are multiple keystrokes before the search dialog takes focus, + // onSearchRequested() will be called for every keystroke, + // but it is idempotent, so it's fine. + return onSearchRequested(); } } return handled; } + private String getTypedText() { + return mDefaultKeySsb.toString(); + } + + private void clearTypedText() { + mDefaultKeySsb.clear(); + mDefaultKeySsb.clearSpans(); + Selection.setSelection(mDefaultKeySsb, 0); + } + /** * Restores the previous state, if it exists. * @@ -495,7 +480,7 @@ public final class Launcher extends Activity implements View.OnClickListener, On drawer.setOnDrawerCloseListener(drawerManager); drawer.setOnDrawerScrollListener(drawerManager); - grid.setTextFilterEnabled(true); + grid.setTextFilterEnabled(false); grid.setDragger(dragLayer); grid.setLauncher(this); @@ -827,7 +812,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On sModel.abortLoaders(); getContentResolver().unregisterContentObserver(mObserver); - getContentResolver().unregisterContentObserver(mAppWidgetResetObserver); unregisterReceiver(mApplicationsReceiver); } @@ -840,13 +824,76 @@ public final class Launcher extends Activity implements View.OnClickListener, On @Override public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { + + closeDrawer(false); + + // Slide the search widget to the top, if it's on the current screen, + // otherwise show the search dialog immediately. + Search searchWidget = mWorkspace.findSearchWidgetOnCurrentScreen(); + if (searchWidget == null) { + showSearchDialog(initialQuery, selectInitialQuery, appSearchData, globalSearch); + } else { + searchWidget.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); + // show the currently typed text in the search widget while sliding + searchWidget.setQuery(getTypedText()); + } + } + + /** + * Show the search dialog immediately, without changing the search widget. + * See {@link Activity.startSearch()} for the arguments. + */ + public void showSearchDialog(String initialQuery, boolean selectInitialQuery, + Bundle appSearchData, boolean globalSearch) { + + if (initialQuery == null) { + // Use any text typed in the launcher as the initial query + initialQuery = getTypedText(); + clearTypedText(); + } if (appSearchData == null) { appSearchData = new Bundle(); appSearchData.putString(SearchManager.SOURCE, "launcher-search"); } - super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); + + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + + final Search searchWidget = mWorkspace.findSearchWidgetOnCurrentScreen(); + if (searchWidget != null) { + // This gets called when the user leaves the search dialog to go back to + // the Launcher. + searchManager.setOnCancelListener(new SearchManager.OnCancelListener() { + public void onCancel() { + searchManager.setOnCancelListener(null); + stopSearch(true); + } + }); + } + + searchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(), + appSearchData, globalSearch); } + /** + * Cancel search dialog if it is open. + * + * @param animate Whether to animate the search gadget (if any) when restoring it + * to its original position. + */ + public void stopSearch(boolean animate) { + // Close search dialog + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + if (searchManager.isVisible()) { + searchManager.stopSearch(); + } + // Restore search widget to its normal position + Search searchWidget = mWorkspace.findSearchWidgetOnCurrentScreen(); + if (searchWidget != null) { + searchWidget.stopSearch(false); + } + } + @Override public boolean onCreateOptionsMenu(Menu menu) { if (mDesktopLocked) return false; @@ -905,15 +952,16 @@ public final class Launcher extends Activity implements View.OnClickListener, On return super.onOptionsItemSelected(item); } + + /** + * Indicates that we want global search for this activity by setting the globalSearch + * argument for {@link #startSearch} to true. + */ @Override public boolean onSearchRequested() { - if (mWorkspace.snapToSearch()) { - closeDrawer(true); // search widget: get drawer out of the way - return true; - } else { - return super.onSearchRequested(); // no search widget: use system search UI - } + startSearch(null, false, null, true); + return true; } private void addItems() { @@ -975,6 +1023,8 @@ public final class Launcher extends Activity implements View.OnClickListener, On final View view = mInflater.inflate(info.layoutResource, null); view.setTag(info); + Search search = (Search) view.findViewById(R.id.widget_search); + search.setLauncher(this); mWorkspace.addInCurrentScreen(view, xy[0], xy[1], info.spanX, spanY); } @@ -1158,7 +1208,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On private void registerContentObservers() { ContentResolver resolver = getContentResolver(); resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, mObserver); - resolver.registerContentObserver(LauncherProvider.CONTENT_APPWIDGET_RESET_URI, true, mAppWidgetResetObserver); } @Override @@ -1224,16 +1273,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On sModel.loadUserItems(false, this, false, false); } - /** - * When reset, we handle by calling {@link AppWidgetHost#startListening()} - * to make sure our callbacks are set correctly. - */ - private void onAppWidgetReset() { - if (mAppWidgetHost != null) { - mAppWidgetHost.startListening(); - } - } - void onDesktopItemsLoaded() { if (mDestroyed) return; bindDesktopItems(); @@ -1250,8 +1289,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On return; } - mAllAppsGrid.setAdapter(drawerAdapter); - final Workspace workspace = mWorkspace; int count = workspace.getChildCount(); for (int i = 0; i < count; i++) { @@ -1275,7 +1312,7 @@ public final class Launcher extends Activity implements View.OnClickListener, On mBinder.mTerminate = true; } - mBinder = new DesktopBinder(this, shortcuts, appWidgets); + mBinder = new DesktopBinder(this, shortcuts, appWidgets, drawerAdapter); mBinder.startBindingItems(); } @@ -1317,6 +1354,9 @@ public final class Launcher extends Activity implements View.OnClickListener, On final View view = mInflater.inflate(R.layout.widget_search, (ViewGroup) workspace.getChildAt(screen), false); + Search search = (Search) view.findViewById(R.id.widget_search); + search.setLauncher(this); + final Widget widget = (Widget) item; view.setTag(widget); @@ -1329,7 +1369,7 @@ public final class Launcher extends Activity implements View.OnClickListener, On if (end >= count) { finishBindDesktopItems(); - binder.startBindingAppWidgetsWhenIdle(); + binder.startBindingDrawer(); } else { binder.obtainMessage(DesktopBinder.MESSAGE_BIND_ITEMS, i, count).sendToTarget(); } @@ -1376,6 +1416,12 @@ public final class Launcher extends Activity implements View.OnClickListener, On mDrawer.unlock(); } + private void bindDrawer(Launcher.DesktopBinder binder, + ApplicationsAdapter drawerAdapter) { + mAllAppsGrid.setAdapter(drawerAdapter); + binder.startBindingAppWidgetsWhenIdle(); + } + private void bindAppWidgets(Launcher.DesktopBinder binder, LinkedList<LauncherAppWidgetInfo> appWidgets) { @@ -1386,14 +1432,10 @@ public final class Launcher extends Activity implements View.OnClickListener, On final LauncherAppWidgetInfo item = appWidgets.removeFirst(); final int appWidgetId = item.appWidgetId; - final AppWidgetProviderInfo appWidgetInfo = - mAppWidgetManager.getAppWidgetInfo(appWidgetId); + final AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); - if (LOGD) { - d(LOG_TAG, String.format("about to setAppWidget for id=%d, info=%s", - appWidgetId, appWidgetInfo)); - } + if (LOGD) d(LOG_TAG, String.format("about to setAppWidget for id=%d, info=%s", appWidgetId, appWidgetInfo)); item.hostView.setAppWidget(appWidgetId, appWidgetInfo); item.hostView.setTag(item); @@ -1891,21 +1933,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On } /** - * Receives notifications when the {@link AppWidgetHost} has been reset, - * usually only when the {@link LauncherProvider} database is first created. - */ - private class AppWidgetResetObserver extends ContentObserver { - public AppWidgetResetObserver() { - super(new Handler()); - } - - @Override - public void onChange(boolean selfChange) { - onAppWidgetReset(); - } - } - - /** * Receives intents from other applications to change the wallpaper. */ private static class WallpaperIntentReceiver extends BroadcastReceiver { @@ -1996,21 +2023,25 @@ public final class Launcher extends Activity implements View.OnClickListener, On private static class DesktopBinder extends Handler implements MessageQueue.IdleHandler { static final int MESSAGE_BIND_ITEMS = 0x1; static final int MESSAGE_BIND_APPWIDGETS = 0x2; + static final int MESSAGE_BIND_DRAWER = 0x3; // Number of items to bind in every pass static final int ITEMS_COUNT = 6; private final ArrayList<ItemInfo> mShortcuts; private final LinkedList<LauncherAppWidgetInfo> mAppWidgets; + private final ApplicationsAdapter mDrawerAdapter; private final WeakReference<Launcher> mLauncher; - public volatile boolean mTerminate = false; + public boolean mTerminate = false; DesktopBinder(Launcher launcher, ArrayList<ItemInfo> shortcuts, - ArrayList<LauncherAppWidgetInfo> appWidgets) { + ArrayList<LauncherAppWidgetInfo> appWidgets, + ApplicationsAdapter drawerAdapter) { mLauncher = new WeakReference<Launcher>(launcher); mShortcuts = shortcuts; + mDrawerAdapter = drawerAdapter; // Sort widgets so active workspace is bound first final int currentScreen = launcher.mWorkspace.getCurrentScreen(); @@ -2030,6 +2061,10 @@ public final class Launcher extends Activity implements View.OnClickListener, On public void startBindingItems() { obtainMessage(MESSAGE_BIND_ITEMS, 0, mShortcuts.size()).sendToTarget(); } + + public void startBindingDrawer() { + obtainMessage(MESSAGE_BIND_DRAWER).sendToTarget(); + } public void startBindingAppWidgetsWhenIdle() { // Ask for notification when message queue becomes idle @@ -2059,6 +2094,10 @@ public final class Launcher extends Activity implements View.OnClickListener, On launcher.bindItems(this, mShortcuts, msg.arg1, msg.arg2); break; } + case MESSAGE_BIND_DRAWER: { + launcher.bindDrawer(this, mDrawerAdapter); + break; + } case MESSAGE_BIND_APPWIDGETS: { launcher.bindAppWidgets(this, mAppWidgets); break; @@ -2067,3 +2106,4 @@ public final class Launcher extends Activity implements View.OnClickListener, On } } } + diff --git a/src/com/android/launcher/Search.java b/src/com/android/launcher/Search.java index 71ab7ef44..3d0947987 100644 --- a/src/com/android/launcher/Search.java +++ b/src/com/android/launcher/Search.java @@ -16,71 +16,59 @@ package com.android.launcher; -import android.app.ISearchManager; -import android.app.SearchManager; 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.Resources; -import android.content.res.Resources.NotFoundException; -import android.database.Cursor; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; +import android.content.res.Configuration; import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.server.search.SearchableInfo; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.View.OnLongClickListener; -import android.widget.AdapterView; -import android.widget.AutoCompleteTextView; -import android.widget.CursorAdapter; -import android.widget.Filter; +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.ImageView; import android.widget.LinearLayout; -import android.widget.SimpleCursorAdapter; import android.widget.TextView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemSelectedListener; -public class Search extends LinearLayout implements OnClickListener, OnKeyListener, - OnLongClickListener, TextWatcher, OnItemClickListener, OnItemSelectedListener { +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 AutoCompleteTextView mSearchText; - private ImageButton mGoButton; + private Launcher mLauncher; + + private TextView mSearchText; private ImageButton mVoiceButton; - private OnLongClickListener mLongClickListener; - - // Support for suggestions - private SuggestionsAdapter mSuggestionsAdapter; - private SearchableInfo mSearchable; - private String mSuggestionAction = null; - private Uri mSuggestionData = null; - private String mSuggestionQuery = null; - private int mItemSelected = -1; - + + /** 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 Rect mTempRect = new Rect(); - private boolean mRestoreFocus = false; - /** * Used to inflate the Workspace from XML. * @@ -89,293 +77,235 @@ public class Search extends LinearLayout implements OnClickListener, OnKeyListen */ 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 (for button) + * Implements OnClickListener. */ public void onClick(View v) { - if (v == mGoButton) { - query(); - } else if (v == mVoiceButton) { - 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"); - } + if (v == mVoiceButton) { + startVoiceSearch(); + } else { + mLauncher.onSearchRequested(); } } - private void query() { - String query = mSearchText.getText().toString(); - if (TextUtils.getTrimmedLength(mSearchText.getText()) == 0) { - return; + 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"); } - Bundle appData = new Bundle(); - appData.putString(SearchManager.SOURCE, "launcher-widget"); - sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, 0, null, mSearchable); - clearQuery(); } - + /** - * Assemble a search intent and send it. - * - * This is copied from SearchDialog. - * - * @param action The intent to send, typically Intent.ACTION_SEARCH - * @param data The data for the intent - * @param query The user text entered (so far) - * @param appData The app data bundle (if supplied) - * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will - * be sent here. Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code. - * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the - * corresponding tag message will be sent here. Pass null for no actionKey message. - * @param si Reference to the current SearchableInfo. Passed here so it can be used even after - * we've called dismiss(), which attempts to null mSearchable. + * 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. */ - private void sendLaunchIntent(final String action, final Uri data, final String query, - final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) { - Intent launcher = new Intent(action); - launcher.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - if (query != null) { - launcher.putExtra(SearchManager.QUERY, query); - } - - if (data != null) { - launcher.setData(data); - } - - if (appData != null) { - launcher.putExtra(SearchManager.APP_DATA, appData); - } - - // add launch info (action key, etc.) - if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { - launcher.putExtra(SearchManager.ACTION_KEY, actionKey); - launcher.putExtra(SearchManager.ACTION_MSG, actionMsg); - } - - // attempt to enforce security requirement (no 3rd-party intents) - if (si != null) { - launcher.setComponent(si.mSearchActivity); - } - - getContext().startActivity(launcher); + public void setQuery(String query) { + mSearchText.setText(query, TextView.BufferType.NORMAL); } - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - if (!hasWindowFocus && hasFocus()) { - mRestoreFocus = true; + /** + * 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); } - - super.onWindowFocusChanged(hasWindowFocus); - - if (hasWindowFocus && mRestoreFocus) { - if (isInTouchMode()) { - final AutoCompleteTextView searchText = mSearchText; - searchText.setSelectAllOnFocus(false); - searchText.requestFocusFromTouch(); - searchText.setSelectAllOnFocus(true); + + if (isAtTop()) { + showSearchDialog(); + } else { + // Start the animation, unless it has already started. + if (getAnimation() != mMorphAnimation) { + mMorphAnimation.setDuration(getAnimationDuration()); + startAnimation(mMorphAnimation); } - mRestoreFocus = false; } } /** - * Implements TextWatcher (for EditText) + * Shows the system search dialog immediately, without any animation. */ - public void beforeTextChanged(CharSequence s, int start, int before, int after) { + private void showSearchDialog() { + mLauncher.showSearchDialog( + mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch); } /** - * Implements TextWatcher (for EditText) + * Restore the search gadget to its normal position. + * + * @param animate Whether to animate the movement of the gadget. */ - public void onTextChanged(CharSequence s, int start, int before, int after) { - // enable the button if we have one or more non-space characters - boolean enabled = TextUtils.getTrimmedLength(mSearchText.getText()) != 0; - mGoButton.setEnabled(enabled); - mGoButton.setFocusable(enabled); + 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(); + } + } } - /** - * Implements TextWatcher (for EditText) - */ - public void afterTextChanged(Editable s) { + private boolean isAtTop() { + return getTop() == 0; + } + + private int getAnimationDuration() { + return (int) (getTop() / ANIMATION_VELOCITY); } /** - * Implements OnKeyListener (for EditText and for button) - * - * This plays some games with state in order to "soften" the strength of suggestions - * presented. Suggestions should not be used unless the user specifically navigates to them - * (or clicks them, in which case it's obvious). This is not the way that AutoCompleteTextBox - * normally works. + * 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. */ - public final boolean onKey(View v, int keyCode, KeyEvent event) { - if (v == mSearchText) { - boolean searchTrigger = (keyCode == KeyEvent.KEYCODE_ENTER || - keyCode == KeyEvent.KEYCODE_SEARCH || - keyCode == KeyEvent.KEYCODE_DPAD_CENTER); - if (event.getAction() == KeyEvent.ACTION_UP) { -// Log.d(TAG, "onKey() ACTION_UP isPopupShowing:" + mSearchText.isPopupShowing()); - if (!mSearchText.isPopupShowing()) { - if (searchTrigger) { - query(); - return true; - } - } + @Override + public void clearAnimation() { + Animation animation = getAnimation(); + if (animation != null) { + super.clearAnimation(); + if (animation.hasEnded() + && animation.getFillAfter() + && animation.willChangeBounds()) { + ((View) getParent()).invalidate(); } else { -// Log.d(TAG, "onKey() ACTION_DOWN isPopupShowing:" + mSearchText.isPopupShowing() + -// " mItemSelected="+ mItemSelected); - if (searchTrigger && mItemSelected < 0) { - query(); - return true; - } + invalidate(); } - } else if (v == mGoButton || v == mVoiceButton) { - boolean handled = false; - 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)) { - if (mSearchText.requestFocus()) { - handled = mSearchText.dispatchKeyEvent(event); - } - } - return handled; } - - return false; - } - - @Override - public void setOnLongClickListener(OnLongClickListener l) { - super.setOnLongClickListener(l); - mLongClickListener = l; } - /** - * Implements OnLongClickListener (for button) - */ - public boolean onLongClick(View v) { - // Pretend that a long press on a child view is a long press on the search widget - if (mLongClickListener != null) { - return mLongClickListener.onLongClick(this); + 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; } - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - // Request focus unless the user tapped on the voice search button - final int x = (int) ev.getX(); - final int y = (int) ev.getY(); - final Rect frame = mTempRect; - mVoiceButton.getHitRect(frame); - if (!frame.contains(x, y)) { - requestFocusFromTouch(); - } - return super.onInterceptTouchEvent(ev); - } - /** - * In order to keep things simple, the external trigger will clear the query just before - * focusing, so as to give you a fresh query. This way we eliminate any sources of - * accidental query launching. + * 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 void clearQuery() { - mSearchText.setText(null); + public boolean onLongClick(View v) { + return performLongClick(); } @Override protected void onFinishInflate() { super.onFinishInflate(); - mSearchText = (AutoCompleteTextView) findViewById(R.id.input); - // TODO: This can be confusing when the user taps the text field to give the focus - // (it is not necessary but I ran into this issue several times myself) - // mTitleInput.setOnClickListener(this); + mSearchText = (TextView) findViewById(R.id.search_src_text); + mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn); + mSearchText.setOnKeyListener(this); - mSearchText.addTextChangedListener(this); - mGoButton = (ImageButton) findViewById(R.id.search_go_btn); - mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn); - mGoButton.setOnClickListener(this); + mSearchText.setOnClickListener(this); mVoiceButton.setOnClickListener(this); - mGoButton.setOnKeyListener(this); - mVoiceButton.setOnKeyListener(this); - + setOnClickListener(this); + mSearchText.setOnLongClickListener(this); - mGoButton.setOnLongClickListener(this); mVoiceButton.setOnLongClickListener(this); - - // disable the button since we start out w/empty input - mGoButton.setEnabled(false); - mGoButton.setFocusable(false); - - configureSearchableInfo(); - configureSuggestions(); + configureVoiceSearchButton(); } - - /** - * Cache of popup padding value after read from {@link Resources}. - */ - private static float mPaddingInset = -1; - - /** - * When our size is changed, pass down adjusted width and offset values to - * correctly center the {@link AutoCompleteTextView} popup and include our - * padding. - */ - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (changed) { - if (mPaddingInset == -1) { - mPaddingInset = getResources().getDimension(R.dimen.search_widget_inset); - } - - // Fill entire width of widget, minus padding inset - float paddedWidth = getWidth() - (mPaddingInset * 2); - float paddedOffset = -(mSearchText.getLeft() - mPaddingInset); - - mSearchText.setDropDownWidth((int) paddedWidth); - mSearchText.setDropDownHorizontalOffset((int) paddedOffset); - } - } - - /** - * Read the searchable info from the search manager - */ - private void configureSearchableInfo() { - ISearchManager sms; - SearchableInfo searchable; - sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE)); - try { - // TODO null isn't the published use of this API, but it works when global=true - // TODO better implementation: defer all of this, let Home set it up - searchable = sms.getSearchableInfo(null, true); - } catch (RemoteException e) { - searchable = null; - } - if (searchable == null) { - // no suggestions so just get out (no need to continue) - return; - } - mSearchable = searchable; - } - + /** * If appropriate & available, configure voice search * @@ -384,346 +314,45 @@ public class Search extends LinearLayout implements OnClickListener, OnKeyListen * voice search. */ private void configureVoiceSearchButton() { - boolean voiceSearchVisible = false; - if (mSearchable.getVoiceSearchEnabled() && mSearchable.getVoiceSearchLaunchWebSearch()) { - // 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); - voiceSearchVisible = ri != null; - } - + // 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); } - - /** The rest of the class deals with providing search suggestions */ - - /** - * Set up the suggestions provider mechanism - */ - private void configureSuggestions() { - // get SearchableInfo - - mSearchText.setOnItemClickListener(this); - mSearchText.setOnItemSelectedListener(this); - - // attach the suggestions adapter - mSuggestionsAdapter = new SuggestionsAdapter(mContext, - com.android.internal.R.layout.search_dropdown_item_2line, null, - SuggestionsAdapter.TWO_LINE_FROM, SuggestionsAdapter.TWO_LINE_TO, mSearchable); - mSearchText.setAdapter(mSuggestionsAdapter); - } - - /** - * Remove internal cursor references when detaching from window which - * prevents {@link Context} leaks. - */ - @Override - public void onDetachedFromWindow() { - if (mSuggestionsAdapter != null) { - mSuggestionsAdapter.changeCursor(null); - mSuggestionsAdapter = null; - } - } - - /** - * Implements OnItemClickListener - */ - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { -// Log.d(TAG, "onItemClick() position " + position); - launchSuggestion(mSuggestionsAdapter, position); - } - - /** - * Implements OnItemSelectedListener - */ - public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { -// Log.d(TAG, "onItemSelected() position " + position); - mItemSelected = position; - } - - /** - * Implements OnItemSelectedListener - */ - public void onNothingSelected(AdapterView<?> parent) { -// Log.d(TAG, "onNothingSelected()"); - mItemSelected = -1; - } /** - * Code to launch a suggestion query. - * - * This is copied from SearchDialog. - * - * @param ca The CursorAdapter containing the suggestions - * @param position The suggestion we'll be launching from - * - * @return Returns true if a successful launch, false if could not (e.g. bad position) + * Sets the {@link Launcher} that this gadget will call on to display the search dialog. */ - private boolean launchSuggestion(CursorAdapter ca, int position) { - if (ca != null) { - Cursor c = ca.getCursor(); - if ((c != null) && c.moveToPosition(position)) { - setupSuggestionIntent(c, mSearchable); - - SearchableInfo si = mSearchable; - String suggestionAction = mSuggestionAction; - Uri suggestionData = mSuggestionData; - String suggestionQuery = mSuggestionQuery; - sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, null, - KeyEvent.KEYCODE_UNKNOWN, null, si); - clearQuery(); - return true; - } - } - return false; - } - - /** - * When a particular suggestion has been selected, perform the various lookups required - * to use the suggestion. This includes checking the cursor for suggestion-specific data, - * and/or falling back to the XML for defaults; It also creates REST style Uri data when - * the suggestion includes a data id. - * - * NOTE: Return values are in member variables mSuggestionAction, mSuggestionData and - * mSuggestionQuery. - * - * This is copied from SearchDialog. - * - * @param c The suggestions cursor, moved to the row of the user's selection - * @param si The searchable activity's info record - */ - void setupSuggestionIntent(Cursor c, SearchableInfo si) { - try { - // use specific action if supplied, or default action if supplied, or fixed default - mSuggestionAction = null; - int column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION); - if (column >= 0) { - final String action = c.getString(column); - if (action != null) { - mSuggestionAction = action; - } - } - if (mSuggestionAction == null) { - mSuggestionAction = si.getSuggestIntentAction(); - } - if (mSuggestionAction == null) { - mSuggestionAction = Intent.ACTION_SEARCH; - } - - // use specific data if supplied, or default data if supplied - String data = null; - column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA); - if (column >= 0) { - final String rowData = c.getString(column); - if (rowData != null) { - data = rowData; - } - } - if (data == null) { - data = si.getSuggestIntentData(); - } - - // then, if an ID was provided, append it. - if (data != null) { - column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); - if (column >= 0) { - final String id = c.getString(column); - if (id != null) { - data = data + "/" + Uri.encode(id); - } - } - } - mSuggestionData = (data == null) ? null : Uri.parse(data); - - mSuggestionQuery = null; - column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY); - if (column >= 0) { - final String query = c.getString(column); - if (query != null) { - mSuggestionQuery = query; - } - } - } catch (RuntimeException e ) { - int rowNum; - try { // be really paranoid now - rowNum = c.getPosition(); - } catch (RuntimeException e2 ) { - rowNum = -1; - } - Log.w(TAG, "Search Suggestions cursor at row " + rowNum + - " returned exception" + e.toString()); - } - } - - SearchAutoCompleteTextView getSearchInputField() { - return (SearchAutoCompleteTextView) mSearchText; + public void setLauncher(Launcher launcher) { + mLauncher = launcher; } - /** - * This class provides the filtering-based interface to suggestions providers. - * It is hardwired in a couple of places to support GoogleSearch - for example, it supports - * two-line suggestions, but it does not support icons. + /** + * Moves the view to the top left corner of its parent. */ - private static class SuggestionsAdapter extends SimpleCursorAdapter { - public final static String[] TWO_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1, - SearchManager.SUGGEST_COLUMN_TEXT_2 }; - public final static int[] TWO_LINE_TO = {com.android.internal.R.id.text1, - com.android.internal.R.id.text2}; - - private final String TAG = "SuggestionsAdapter"; - - Filter mFilter; - SearchableInfo mSearchable; - private Resources mProviderResources; - String[] mFromStrings; - - public SuggestionsAdapter(Context context, int layout, Cursor c, - String[] from, int[] to, SearchableInfo searchable) { - super(context, layout, c, from, to); - mFromStrings = from; - mSearchable = searchable; - - // set up provider resources (gives us icons, etc.) - Context activityContext = mSearchable.getActivityContext(mContext); - Context providerContext = mSearchable.getProviderContext(mContext, activityContext); - mProviderResources = providerContext.getResources(); - } - - /** - * Use the search suggestions provider to obtain a live cursor. This will be called - * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions). - * The results will be processed in the UI thread and changeCursor() will be called. - */ + private class ToParentOriginAnimation extends Animation { @Override - public Cursor runQueryOnBackgroundThread(CharSequence constraint) { - String query = (constraint == null) ? "" : constraint.toString(); - return getSuggestions(mSearchable, query); - } - - /** - * Overriding this allows us to write the selected query back into the box. - * NOTE: This is a vastly simplified version of SearchDialog.jamQuery() and does - * not universally support the search API. But it is sufficient for Google Search. - */ - @Override - public CharSequence convertToString(Cursor cursor) { - CharSequence result = null; - if (cursor != null) { - int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY); - if (column >= 0) { - final String query = cursor.getString(column); - if (query != null) { - result = query; - } - } - } - return result; - } - - /** - * Get the query cursor for the search suggestions. - * - * TODO this is functionally identical to the version in SearchDialog.java. Perhaps it - * could be hoisted into SearchableInfo or some other shared spot. - * - * @param query The search text entered (so far) - * @return Returns a cursor with suggestions, or null if no suggestions - */ - private Cursor getSuggestions(final SearchableInfo searchable, final String query) { - Cursor cursor = null; - if (searchable.getSuggestAuthority() != null) { - try { - StringBuilder uriStr = new StringBuilder("content://"); - uriStr.append(searchable.getSuggestAuthority()); - - // if content path provided, insert it now - final String contentPath = searchable.getSuggestPath(); - if (contentPath != null) { - uriStr.append('/'); - uriStr.append(contentPath); - } - - // append standard suggestion query path - uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY); - - // inject query, either as selection args or inline - String[] selArgs = null; - if (searchable.getSuggestSelection() != null) { // use selection if provided - selArgs = new String[] {query}; - } else { - uriStr.append('/'); // no sel, use REST pattern - uriStr.append(Uri.encode(query)); - } - - // finally, make the query - cursor = mContext.getContentResolver().query( - Uri.parse(uriStr.toString()), null, - searchable.getSuggestSelection(), selArgs, - null); - } catch (RuntimeException e) { - Log.w(TAG, "Search Suggestions query returned exception " + e.toString()); - cursor = null; - } - } - - return cursor; + protected void applyTransformation(float interpolatedTime, Transformation t) { + float dx = -getLeft() * interpolatedTime; + float dy = -getTop() * interpolatedTime; + t.getMatrix().setTranslate(dx, dy); } + } - /** - * Overriding this allows us to affect the way that an icon is loaded. Specifically, - * we can be more controlling about the resource path (and allow icons to come from other - * packages). - * - * TODO: This is 100% identical to the version in SearchDialog.java - * - * @param v ImageView to receive an image - * @param value the value retrieved from the cursor - */ + /** + * Moves the view from the top left corner of its parent. + */ + private class FromParentOriginAnimation extends Animation { @Override - public void setViewImage(ImageView v, String value) { - int resID; - Drawable img = null; - - try { - resID = Integer.parseInt(value); - if (resID != 0) { - img = mProviderResources.getDrawable(resID); - } - } catch (NumberFormatException nfe) { - // img = null; - } catch (NotFoundException e2) { - // img = null; - } - - // finally, set the image to whatever we've gotten - v.setImageDrawable(img); + protected void applyTransformation(float interpolatedTime, Transformation t) { + float dx = -getLeft() * (1.0f - interpolatedTime); + float dy = -getTop() * (1.0f - interpolatedTime); + t.getMatrix().setTranslate(dx, dy); } - - /** - * This method is overridden purely to provide a bit of protection against - * flaky content providers. - * - * TODO: This is 100% identical to the version in SearchDialog.java - * - * @see android.widget.ListAdapter#getView(int, View, ViewGroup) - */ - @Override - public View getView(int position, View convertView, ViewGroup parent) { - try { - return super.getView(position, convertView, parent); - } catch (RuntimeException e) { - Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString()); - // what can I return here? - View v = newView(mContext, mCursor, parent); - if (v != null) { - TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1); - tv.setText(e.toString()); - } - return v; - } - } - } + } diff --git a/src/com/android/launcher/Workspace.java b/src/com/android/launcher/Workspace.java index 12bdf7d0a..d474efa60 100644 --- a/src/com/android/launcher/Workspace.java +++ b/src/com/android/launcher/Workspace.java @@ -1100,7 +1100,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } return result; } - + /** * Find a search widget on the given screen */ @@ -1114,102 +1114,14 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } return null; } - - /** - * Focuses on the search widget on the specified screen, - * if there is one. Also clears the current search selection so we don't - */ - private boolean focusOnSearch(int screen) { - CellLayout currentScreen = (CellLayout) getChildAt(screen); - final Search searchWidget = findSearchWidget(currentScreen); - if (searchWidget != null) { - // This is necessary when focus on search is requested from the menu - // If the workspace was not in touch mode before the menu is invoked - // and the user clicks "Search" by touching the menu item, the following - // happens: - // - // - We request focus from touch on the search widget - // - The search widget gains focus - // - The window focus comes back to Home's window - // - The touch mode change is propagated to Home's window - // - The search widget is not focusable in touch mode and ViewRoot - // clears its focus - // - // Forcing focusable in touch mode ensures the search widget will - // keep the focus no matter what happens. - // - // Note: the search input field disables focusable in touch mode - // after the window gets the focus back, see SearchAutoCompleteTextView - final SearchAutoCompleteTextView input = searchWidget.getSearchInputField(); - input.setFocusableInTouchMode(true); - input.showKeyboardOnNextFocus(); - - if (isInTouchMode()) { - searchWidget.requestFocusFromTouch(); - } else { - searchWidget.requestFocus(); - } - searchWidget.clearQuery(); - return true; - } - return false; - } - + /** - * Snap to the nearest screen with a search widget and give it focus - * - * @return True if a search widget was found + * Gets the first search widget on the current screen, if there is one. + * Returns <code>null</code> otherwise. */ - public boolean snapToSearch() { - // The screen we are searching - int current = mCurrentScreen; - - // first position scanned so far - int first = current; - - // last position scanned so far - int last = current; - - // True if we should move down on the next iteration - boolean next = false; - - // True when we have looked at the first item in the data - boolean hitFirst; - - // True when we have looked at the last item in the data - boolean hitLast; - - final int count = getChildCount(); - - while (true) { - if (focusOnSearch(current)) { - return true; - } - - hitLast = last == count - 1; - hitFirst = first == 0; - - if (hitLast && hitFirst) { - // Looked at everything - break; - } - - if (hitFirst || (next && !hitLast)) { - // Either we hit the top, or we are trying to move down - last++; - current = last; - // Try going up next time - next = false; - } else { - // Either we hit the bottom, or we are trying to move up - first--; - current = first; - // Try going down next time - next = true; - } - - } - return false; + public Search findSearchWidgetOnCurrentScreen() { + CellLayout currentScreen = (CellLayout)getChildAt(mCurrentScreen); + return findSearchWidget(currentScreen); } public Folder getFolderForTag(Object tag) { |