1548 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			1548 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| /*
 | |
|  * 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 android.app;
 | |
| 
 | |
| 
 | |
| import static android.app.SuggestionsAdapter.getColumnString;
 | |
| 
 | |
| import java.util.WeakHashMap;
 | |
| import java.util.concurrent.atomic.AtomicLong;
 | |
| 
 | |
| 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.ActivityInfo;
 | |
| import android.content.pm.PackageManager;
 | |
| import android.content.pm.ResolveInfo;
 | |
| import android.content.pm.PackageManager.NameNotFoundException;
 | |
| import android.content.res.Configuration;
 | |
| import android.content.res.Resources;
 | |
| import android.database.Cursor;
 | |
| import android.graphics.drawable.Drawable;
 | |
| import android.net.Uri;
 | |
| import android.os.Bundle;
 | |
| import android.os.SystemClock;
 | |
| import android.provider.Browser;
 | |
| import android.speech.RecognizerIntent;
 | |
| import android.text.Editable;
 | |
| import android.text.InputType;
 | |
| import android.text.TextUtils;
 | |
| import android.text.TextWatcher;
 | |
| import android.util.AttributeSet;
 | |
| import android.util.Log;
 | |
| import android.view.Gravity;
 | |
| import android.view.KeyEvent;
 | |
| import android.view.MotionEvent;
 | |
| import android.view.View;
 | |
| import android.view.ViewConfiguration;
 | |
| import android.view.ViewGroup;
 | |
| import android.view.Window;
 | |
| import android.view.WindowManager;
 | |
| import android.view.inputmethod.EditorInfo;
 | |
| import android.view.inputmethod.InputMethodManager;
 | |
| import android.widget.AdapterView;
 | |
| import android.widget.AutoCompleteTextView;
 | |
| import android.widget.Button;
 | |
| import android.widget.ImageButton;
 | |
| import android.widget.ImageView;
 | |
| import android.widget.LinearLayout;
 | |
| import android.widget.ListView;
 | |
| import android.widget.TextView;
 | |
| import android.widget.AdapterView.OnItemClickListener;
 | |
| import android.widget.AdapterView.OnItemSelectedListener;
 | |
| 
 | |
| /**
 | |
|  * Search dialog. This is controlled by the 
 | |
|  * SearchManager and runs in the current foreground process.
 | |
|  * 
 | |
|  * @hide
 | |
|  */
 | |
| public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
 | |
| 
 | |
|     // Debugging support
 | |
|     private static final boolean DBG = false;
 | |
|     private static final String LOG_TAG = "SearchDialog";
 | |
|     private static final boolean DBG_LOG_TIMING = false;
 | |
| 
 | |
|     private static final String INSTANCE_KEY_COMPONENT = "comp";
 | |
|     private static final String INSTANCE_KEY_APPDATA = "data";
 | |
|     private static final String INSTANCE_KEY_STORED_APPDATA = "sData";
 | |
|     private static final String INSTANCE_KEY_USER_QUERY = "uQry";
 | |
|     
 | |
|     // The string used for privateImeOptions to identify to the IME that it should not show
 | |
|     // a microphone button since one already exists in the search dialog.
 | |
|     private static final String IME_OPTION_NO_MICROPHONE = "nm";
 | |
| 
 | |
|     private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12;
 | |
|     private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
 | |
| 
 | |
|     // views & widgets
 | |
|     private TextView mBadgeLabel;
 | |
|     private ImageView mAppIcon;
 | |
|     private SearchAutoComplete mSearchAutoComplete;
 | |
|     private Button mGoButton;
 | |
|     private ImageButton mVoiceButton;
 | |
|     private View mSearchPlate;
 | |
|     private Drawable mWorkingSpinner;
 | |
| 
 | |
|     // interaction with searchable application
 | |
|     private SearchableInfo mSearchable;
 | |
|     private ComponentName mLaunchComponent;
 | |
|     private Bundle mAppSearchData;
 | |
|     private Context mActivityContext;
 | |
|     private SearchManager mSearchManager;
 | |
| 
 | |
|     // For voice searching
 | |
|     private final Intent mVoiceWebSearchIntent;
 | |
|     private final Intent mVoiceAppSearchIntent;
 | |
| 
 | |
|     // support for AutoCompleteTextView suggestions display
 | |
|     private SuggestionsAdapter mSuggestionsAdapter;
 | |
|     
 | |
|     // Whether to rewrite queries when selecting suggestions
 | |
|     private static final boolean REWRITE_QUERIES = true;
 | |
|     
 | |
|     // The query entered by the user. This is not changed when selecting a suggestion
 | |
|     // that modifies the contents of the text field. But if the user then edits
 | |
|     // the suggestion, the resulting string is saved.
 | |
|     private String mUserQuery;
 | |
|     // The query passed in when opening the SearchDialog.  Used in the browser
 | |
|     // case to determine whether the user has edited the query.
 | |
|     private String mInitialQuery;
 | |
|     
 | |
|     // A weak map of drawables we've gotten from other packages, so we don't load them
 | |
|     // more than once.
 | |
|     private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
 | |
|             new WeakHashMap<String, Drawable.ConstantState>();
 | |
| 
 | |
|     // Last known IME options value for the search edit text.
 | |
|     private int mSearchAutoCompleteImeOptions;
 | |
| 
 | |
|     private BroadcastReceiver mConfChangeListener = new BroadcastReceiver() {
 | |
|         @Override
 | |
|         public void onReceive(Context context, Intent intent) {
 | |
|             if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
 | |
|                 onConfigurationChanged();
 | |
|             }
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * Constructor - fires it up and makes it look like the search UI.
 | |
|      * 
 | |
|      * @param context Application Context we can use for system acess
 | |
|      */
 | |
|     public SearchDialog(Context context, SearchManager searchManager) {
 | |
|         super(context, com.android.internal.R.style.Theme_SearchBar);
 | |
| 
 | |
|         // Save voice intent for later queries/launching
 | |
|         mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
 | |
|         mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 | |
|         mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
 | |
|                 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
 | |
| 
 | |
|         mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
 | |
|         mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 | |
|         mSearchManager = searchManager;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create the search dialog and any resources that are used for the
 | |
|      * entire lifetime of the dialog.
 | |
|      */
 | |
|     @Override
 | |
|     protected void onCreate(Bundle savedInstanceState) {
 | |
|         super.onCreate(savedInstanceState);
 | |
| 
 | |
|         Window theWindow = getWindow();
 | |
|         WindowManager.LayoutParams lp = theWindow.getAttributes();
 | |
|         lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
 | |
|         // taking up the whole window (even when transparent) is less than ideal,
 | |
|         // but necessary to show the popup window until the window manager supports
 | |
|         // having windows anchored by their parent but not clipped by them.
 | |
|         lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
 | |
|         lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
 | |
|         lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 | |
|         theWindow.setAttributes(lp);
 | |
| 
 | |
|         // Touching outside of the search dialog will dismiss it
 | |
|         setCanceledOnTouchOutside(true);        
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * We recreate the dialog view each time it becomes visible so as to limit
 | |
|      * the scope of any problems with the contained resources.
 | |
|      */
 | |
|     private void createContentView() {
 | |
|         setContentView(com.android.internal.R.layout.search_bar);
 | |
| 
 | |
|         // get the view elements for local access
 | |
|         SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar);
 | |
|         searchBar.setSearchDialog(this);
 | |
| 
 | |
|         mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
 | |
|         mSearchAutoComplete = (SearchAutoComplete)
 | |
|                 findViewById(com.android.internal.R.id.search_src_text);
 | |
|         mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
 | |
|         mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
 | |
|         mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
 | |
|         mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
 | |
|         mWorkingSpinner = getContext().getResources().
 | |
|                 getDrawable(com.android.internal.R.drawable.search_spinner);
 | |
|         mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
 | |
|                 null, null, mWorkingSpinner, null);
 | |
|         setWorking(false);
 | |
| 
 | |
|         // attach listeners
 | |
|         mSearchAutoComplete.addTextChangedListener(mTextWatcher);
 | |
|         mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
 | |
|         mSearchAutoComplete.setOnItemClickListener(this);
 | |
|         mSearchAutoComplete.setOnItemSelectedListener(this);
 | |
|         mGoButton.setOnClickListener(mGoButtonClickListener);
 | |
|         mGoButton.setOnKeyListener(mButtonsKeyListener);
 | |
|         mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
 | |
|         mVoiceButton.setOnKeyListener(mButtonsKeyListener);
 | |
| 
 | |
|         // pre-hide all the extraneous elements
 | |
|         mBadgeLabel.setVisibility(View.GONE);
 | |
| 
 | |
|         // Additional adjustments to make Dialog work for Search
 | |
|         mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set up the search dialog
 | |
|      * 
 | |
|      * @return true if search dialog launched, false if not
 | |
|      */
 | |
|     public boolean show(String initialQuery, boolean selectInitialQuery,
 | |
|             ComponentName componentName, Bundle appSearchData) {
 | |
|         boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData);
 | |
|         if (success) {
 | |
|             // Display the drop down as soon as possible instead of waiting for the rest of the
 | |
|             // pending UI stuff to get done, so that things appear faster to the user.
 | |
|             mSearchAutoComplete.showDropDownAfterLayout();
 | |
|         }
 | |
|         return success;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Does the rest of the work required to show the search dialog. Called by
 | |
|      * {@link #show(String, boolean, ComponentName, Bundle)} and
 | |
|      *
 | |
|      * @return true if search dialog showed, false if not
 | |
|      */
 | |
|     private boolean doShow(String initialQuery, boolean selectInitialQuery,
 | |
|             ComponentName componentName, Bundle appSearchData) {
 | |
|         // set up the searchable and show the dialog
 | |
|         if (!show(componentName, appSearchData)) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         mInitialQuery = initialQuery == null ? "" : initialQuery;
 | |
|         // finally, load the user's initial text (which may trigger suggestions)
 | |
|         setUserQuery(initialQuery);
 | |
|         if (selectInitialQuery) {
 | |
|             mSearchAutoComplete.selectAll();
 | |
|         }
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sets up the search dialog and shows it.
 | |
|      * 
 | |
|      * @return <code>true</code> if search dialog launched
 | |
|      */
 | |
|     private boolean show(ComponentName componentName, Bundle appSearchData) {
 | |
|         
 | |
|         if (DBG) { 
 | |
|             Log.d(LOG_TAG, "show(" + componentName + ", " 
 | |
|                     + appSearchData + ")");
 | |
|         }
 | |
|         
 | |
|         SearchManager searchManager = (SearchManager)
 | |
|                 mContext.getSystemService(Context.SEARCH_SERVICE);
 | |
|         // Try to get the searchable info for the provided component.
 | |
|         mSearchable = searchManager.getSearchableInfo(componentName);
 | |
| 
 | |
|         if (mSearchable == null) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         mLaunchComponent = componentName;
 | |
|         mAppSearchData = appSearchData;
 | |
|         mActivityContext = mSearchable.getActivityContext(getContext());
 | |
| 
 | |
|         // show the dialog. this will call onStart().
 | |
|         if (!isShowing()) {
 | |
|             // Recreate the search bar view every time the dialog is shown, to get rid
 | |
|             // of any bad state in the AutoCompleteTextView etc
 | |
|             createContentView();
 | |
| 
 | |
|             show();
 | |
|         }
 | |
|         updateUI();
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     @Override
 | |
|     public void onStart() {
 | |
|         super.onStart();
 | |
| 
 | |
|         // Register a listener for configuration change events.
 | |
|         IntentFilter filter = new IntentFilter();
 | |
|         filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
 | |
|         getContext().registerReceiver(mConfChangeListener, filter);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * The search dialog is being dismissed, so handle all of the local shutdown operations.
 | |
|      * 
 | |
|      * This function is designed to be idempotent so that dismiss() can be safely called at any time
 | |
|      * (even if already closed) and more likely to really dump any memory.  No leaks!
 | |
|      */
 | |
|     @Override
 | |
|     public void onStop() {
 | |
|         super.onStop();
 | |
| 
 | |
|         getContext().unregisterReceiver(mConfChangeListener);
 | |
| 
 | |
|         closeSuggestionsAdapter();
 | |
|         
 | |
|         // dump extra memory we're hanging on to
 | |
|         mLaunchComponent = null;
 | |
|         mAppSearchData = null;
 | |
|         mSearchable = null;
 | |
|         mUserQuery = null;
 | |
|         mInitialQuery = null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sets the search dialog to the 'working' state, which shows a working spinner in the
 | |
|      * right hand size of the text field.
 | |
|      * 
 | |
|      * @param working true to show spinner, false to hide spinner
 | |
|      */
 | |
|     public void setWorking(boolean working) {
 | |
|         mWorkingSpinner.setAlpha(working ? 255 : 0);
 | |
|         mWorkingSpinner.setVisible(working, false);
 | |
|         mWorkingSpinner.invalidateSelf();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Closes and gets rid of the suggestions adapter.
 | |
|      */
 | |
|     private void closeSuggestionsAdapter() {
 | |
|         // remove the adapter from the autocomplete first, to avoid any updates
 | |
|         // when we drop the cursor
 | |
|         mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
 | |
|         // close any leftover cursor
 | |
|         if (mSuggestionsAdapter != null) {
 | |
|             mSuggestionsAdapter.close();
 | |
|         }
 | |
|         mSuggestionsAdapter = null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Save the minimal set of data necessary to recreate the search
 | |
|      * 
 | |
|      * @return A bundle with the state of the dialog, or {@code null} if the search
 | |
|      *         dialog is not showing.
 | |
|      */
 | |
|     @Override
 | |
|     public Bundle onSaveInstanceState() {
 | |
|         if (!isShowing()) return null;
 | |
| 
 | |
|         Bundle bundle = new Bundle();
 | |
| 
 | |
|         // setup info so I can recreate this particular search       
 | |
|         bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
 | |
|         bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
 | |
|         bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
 | |
| 
 | |
|         return bundle;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Restore the state of the dialog from a previously saved bundle.
 | |
|      * 
 | |
|      * TODO: go through this and make sure that it saves everything that is saved
 | |
|      *
 | |
|      * @param savedInstanceState The state of the dialog previously saved by
 | |
|      *     {@link #onSaveInstanceState()}.
 | |
|      */
 | |
|     @Override
 | |
|     public void onRestoreInstanceState(Bundle savedInstanceState) {
 | |
|         if (savedInstanceState == null) return;
 | |
| 
 | |
|         ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
 | |
|         Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
 | |
|         String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
 | |
| 
 | |
|         // show the dialog.
 | |
|         if (!doShow(userQuery, false, launchComponent, appSearchData)) {
 | |
|             // for some reason, we couldn't re-instantiate
 | |
|             return;
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Called after resources have changed, e.g. after screen rotation or locale change.
 | |
|      */
 | |
|     public void onConfigurationChanged() {
 | |
|         if (mSearchable != null && isShowing()) {
 | |
|             // Redraw (resources may have changed)
 | |
|             updateSearchButton();
 | |
|             updateSearchAppIcon();
 | |
|             updateSearchBadge();
 | |
|             updateQueryHint();
 | |
|             if (isLandscapeMode(getContext())) {
 | |
|                 mSearchAutoComplete.ensureImeVisible(true);
 | |
|             }
 | |
|             mSearchAutoComplete.showDropDownAfterLayout();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     static boolean isLandscapeMode(Context context) {
 | |
|         return context.getResources().getConfiguration().orientation
 | |
|                 == Configuration.ORIENTATION_LANDSCAPE;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Update the UI according to the info in the current value of {@link #mSearchable}.
 | |
|      */
 | |
|     private void updateUI() {
 | |
|         if (mSearchable != null) {
 | |
|             mDecor.setVisibility(View.VISIBLE);
 | |
|             updateSearchAutoComplete();
 | |
|             updateSearchButton();
 | |
|             updateSearchAppIcon();
 | |
|             updateSearchBadge();
 | |
|             updateQueryHint();
 | |
|             updateVoiceButton(TextUtils.isEmpty(mUserQuery));
 | |
|             
 | |
|             // In order to properly configure the input method (if one is being used), we
 | |
|             // need to let it know if we'll be providing suggestions.  Although it would be
 | |
|             // difficult/expensive to know if every last detail has been configured properly, we 
 | |
|             // can at least see if a suggestions provider has been configured, and use that
 | |
|             // as our trigger.
 | |
|             int inputType = mSearchable.getInputType();
 | |
|             // We only touch this if the input type is set up for text (which it almost certainly
 | |
|             // should be, in the case of search!)
 | |
|             if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
 | |
|                 // The existence of a suggestions authority is the proxy for "suggestions 
 | |
|                 // are available here"
 | |
|                 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
 | |
|                 if (mSearchable.getSuggestAuthority() != null) {
 | |
|                     inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
 | |
|                 }
 | |
|             }
 | |
|             mSearchAutoComplete.setInputType(inputType);
 | |
|             mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
 | |
|             mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
 | |
|             
 | |
|             // If the search dialog is going to show a voice search button, then don't let
 | |
|             // the soft keyboard display a microphone button if it would have otherwise.
 | |
|             if (mSearchable.getVoiceSearchEnabled()) {
 | |
|                 mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
 | |
|             } else {
 | |
|                 mSearchAutoComplete.setPrivateImeOptions(null);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Updates the auto-complete text view.
 | |
|      */
 | |
|     private void updateSearchAutoComplete() {
 | |
|         // close any existing suggestions adapter
 | |
|         closeSuggestionsAdapter();
 | |
|         
 | |
|         mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
 | |
|         mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
 | |
|         // we dismiss the entire dialog instead
 | |
|         mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
 | |
| 
 | |
|         mSearchAutoComplete.setForceIgnoreOutsideTouch(true);
 | |
| 
 | |
|         // attach the suggestions adapter, if suggestions are available
 | |
|         // The existence of a suggestions authority is the proxy for "suggestions available here"
 | |
|         if (mSearchable.getSuggestAuthority() != null) {
 | |
|             mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable, 
 | |
|                     mOutsideDrawablesCache);
 | |
|             mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private void updateSearchButton() {
 | |
|         String textLabel = null;
 | |
|         Drawable iconLabel = null;
 | |
|         int textId = mSearchable.getSearchButtonText(); 
 | |
|         if (isBrowserSearch()){
 | |
|             iconLabel = getContext().getResources()
 | |
|                     .getDrawable(com.android.internal.R.drawable.ic_btn_search_go);
 | |
|         } else if (textId != 0) {
 | |
|             textLabel = mActivityContext.getResources().getString(textId);  
 | |
|         } else {
 | |
|             iconLabel = getContext().getResources().
 | |
|                     getDrawable(com.android.internal.R.drawable.ic_btn_search);
 | |
|         }
 | |
|         mGoButton.setText(textLabel);
 | |
|         mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
 | |
|     }
 | |
|     
 | |
|     private void updateSearchAppIcon() {
 | |
|         if (isBrowserSearch()) {
 | |
|             mAppIcon.setImageResource(0);
 | |
|             mAppIcon.setVisibility(View.GONE);
 | |
|             mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL,
 | |
|                     mSearchPlate.getPaddingTop(),
 | |
|                     mSearchPlate.getPaddingRight(),
 | |
|                     mSearchPlate.getPaddingBottom());
 | |
|         } else {
 | |
|             PackageManager pm = getContext().getPackageManager();
 | |
|             Drawable icon;
 | |
|             try {
 | |
|                 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
 | |
|                 icon = pm.getApplicationIcon(info.applicationInfo);
 | |
|                 if (DBG) Log.d(LOG_TAG, "Using app-specific icon");
 | |
|             } catch (NameNotFoundException e) {
 | |
|                 icon = pm.getDefaultActivityIcon();
 | |
|                 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
 | |
|             }
 | |
|             mAppIcon.setImageDrawable(icon);
 | |
|             mAppIcon.setVisibility(View.VISIBLE);
 | |
|             mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL,
 | |
|                     mSearchPlate.getPaddingTop(),
 | |
|                     mSearchPlate.getPaddingRight(),
 | |
|                     mSearchPlate.getPaddingBottom());
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Setup the search "Badge" if requested by mode flags.
 | |
|      */
 | |
|     private void updateSearchBadge() {
 | |
|         // assume both hidden
 | |
|         int visibility = View.GONE;
 | |
|         Drawable icon = null;
 | |
|         CharSequence text = null;
 | |
|         
 | |
|         // optionally show one or the other.
 | |
|         if (mSearchable.useBadgeIcon()) {
 | |
|             icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
 | |
|             visibility = View.VISIBLE;
 | |
|             if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
 | |
|         } else if (mSearchable.useBadgeLabel()) {
 | |
|             text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
 | |
|             visibility = View.VISIBLE;
 | |
|             if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
 | |
|         }
 | |
|         
 | |
|         mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
 | |
|         mBadgeLabel.setText(text);
 | |
|         mBadgeLabel.setVisibility(visibility);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Update the hint in the query text field.
 | |
|      */
 | |
|     private void updateQueryHint() {
 | |
|         if (isShowing()) {
 | |
|             String hint = null;
 | |
|             if (mSearchable != null) {
 | |
|                 int hintId = mSearchable.getHintId();
 | |
|                 if (hintId != 0) {
 | |
|                     hint = mActivityContext.getString(hintId);
 | |
|                 }
 | |
|             }
 | |
|             mSearchAutoComplete.setHint(hint);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Update the visibility of the voice button.  There are actually two voice search modes, 
 | |
|      * either of which will activate the button.
 | |
|      * @param empty whether the search query text field is empty. If it is, then the other
 | |
|      * criteria apply to make the voice button visible. Otherwise the voice button will not
 | |
|      * be visible - i.e., if the user has typed a query, remove the voice button.
 | |
|      */
 | |
|     private void updateVoiceButton(boolean empty) {
 | |
|         int visibility = View.GONE;
 | |
|         if ((mAppSearchData == null || !mAppSearchData.getBoolean(
 | |
|                 SearchManager.DISABLE_VOICE_SEARCH, false))
 | |
|                 && mSearchable.getVoiceSearchEnabled() && empty) {
 | |
|             Intent testIntent = null;
 | |
|             if (mSearchable.getVoiceSearchLaunchWebSearch()) {
 | |
|                 testIntent = mVoiceWebSearchIntent;
 | |
|             } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
 | |
|                 testIntent = mVoiceAppSearchIntent;
 | |
|             }      
 | |
|             if (testIntent != null) {
 | |
|                 ResolveInfo ri = getContext().getPackageManager().
 | |
|                         resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
 | |
|                 if (ri != null) {
 | |
|                     visibility = View.VISIBLE;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         mVoiceButton.setVisibility(visibility);
 | |
|     }
 | |
| 
 | |
|     /** Called by SuggestionsAdapter when the cursor contents changed. */
 | |
|     void onDataSetChanged() {
 | |
|         if (mSearchAutoComplete != null && mSuggestionsAdapter != null) {
 | |
|             mSearchAutoComplete.onFilterComplete(mSuggestionsAdapter.getCount());
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Hack to determine whether this is the browser, so we can adjust the UI.
 | |
|      */
 | |
|     private boolean isBrowserSearch() {
 | |
|         return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/");
 | |
|     }
 | |
| 
 | |
|     /*
 | |
|      * Listeners of various types
 | |
|      */
 | |
| 
 | |
|     /**
 | |
|      * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
 | |
|      * touch is outside the window. But the window includes space for the drop-down,
 | |
|      * so we also cancel on taps outside the search bar when the drop-down is not showing.
 | |
|      */
 | |
|     @Override
 | |
|     public boolean onTouchEvent(MotionEvent event) {
 | |
|         // cancel if the drop-down is not showing and the touch event was outside the search plate
 | |
|         if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
 | |
|             if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
 | |
|             cancel();
 | |
|             return true;
 | |
|         }
 | |
|         // Let Dialog handle events outside the window while the pop-up is showing.
 | |
|         return super.onTouchEvent(event);
 | |
|     }
 | |
|     
 | |
|     private boolean isOutOfBounds(View v, MotionEvent event) {
 | |
|         final int x = (int) event.getX();
 | |
|         final int y = (int) event.getY();
 | |
|         final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
 | |
|         return (x < -slop) || (y < -slop)
 | |
|                 || (x > (v.getWidth()+slop))
 | |
|                 || (y > (v.getHeight()+slop));
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Dialog's OnKeyListener implements various search-specific functionality
 | |
|      *
 | |
|      * @param keyCode This is the keycode of the typed key, and is the same value as
 | |
|      *        found in the KeyEvent parameter.
 | |
|      * @param event The complete event record for the typed key
 | |
|      *
 | |
|      * @return Return true if the event was handled here, or false if not.
 | |
|      */
 | |
|     @Override
 | |
|     public boolean onKeyDown(int keyCode, KeyEvent event) {
 | |
|         if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
 | |
|         if (mSearchable == null) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         // if it's an action specified by the searchable activity, launch the
 | |
|         // entered query with the action key
 | |
|         SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
 | |
|         if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
 | |
|             launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         return super.onKeyDown(keyCode, event);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Callback to watch the textedit field for empty/non-empty
 | |
|      */
 | |
|     private TextWatcher mTextWatcher = new TextWatcher() {
 | |
| 
 | |
|         public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
 | |
| 
 | |
|         public void onTextChanged(CharSequence s, int start,
 | |
|                 int before, int after) {
 | |
|             if (DBG_LOG_TIMING) {
 | |
|                 dbgLogTiming("onTextChanged()");
 | |
|             }
 | |
|             if (mSearchable == null) {
 | |
|                 return;
 | |
|             }
 | |
|             if (!mSearchAutoComplete.isPerformingCompletion()) {
 | |
|                 // The user changed the query, remember it.
 | |
|                 mUserQuery = s == null ? "" : s.toString();
 | |
|             }
 | |
|             updateWidgetState();
 | |
|             // Always want to show the microphone if the context is voice.
 | |
|             // Also show the microphone if this is a browser search and the
 | |
|             // query matches the initial query.
 | |
|             updateVoiceButton(mSearchAutoComplete.isEmpty()
 | |
|                     || (isBrowserSearch() && mInitialQuery.equals(mUserQuery))
 | |
|                     || (mAppSearchData != null && mAppSearchData.getBoolean(
 | |
|                     SearchManager.CONTEXT_IS_VOICE)));
 | |
|         }
 | |
| 
 | |
|         public void afterTextChanged(Editable s) {
 | |
|             if (mSearchable == null) {
 | |
|                 return;
 | |
|             }
 | |
|             if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) {
 | |
|                 // The user changed the query, check if it is a URL and if so change the search
 | |
|                 // button in the soft keyboard to the 'Go' button.
 | |
|                 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION))
 | |
|                         | EditorInfo.IME_ACTION_GO;
 | |
|                 if (options != mSearchAutoCompleteImeOptions) {
 | |
|                     mSearchAutoCompleteImeOptions = options;
 | |
|                     mSearchAutoComplete.setImeOptions(options);
 | |
|                     // This call is required to update the soft keyboard UI with latest IME flags.
 | |
|                     mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType());
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * Enable/Disable the go button based on edit text state (any text?)
 | |
|      */
 | |
|     private void updateWidgetState() {
 | |
|         // enable the button if we have one or more non-space characters
 | |
|         boolean enabled = !mSearchAutoComplete.isEmpty();
 | |
|         if (isBrowserSearch()) {
 | |
|             // In the browser, we hide the search button when there is no text,
 | |
|             // or if the text matches the initial query.
 | |
|             if (enabled && !mInitialQuery.equals(mUserQuery)) {
 | |
|                 mSearchAutoComplete.setBackgroundResource(
 | |
|                         com.android.internal.R.drawable.textfield_search);
 | |
|                 mGoButton.setVisibility(View.VISIBLE);
 | |
|                 // Just to be sure
 | |
|                 mGoButton.setEnabled(true);
 | |
|                 mGoButton.setFocusable(true);
 | |
|             } else {
 | |
|                 mSearchAutoComplete.setBackgroundResource(
 | |
|                         com.android.internal.R.drawable.textfield_search_empty);
 | |
|                 mGoButton.setVisibility(View.GONE);
 | |
|             }
 | |
|         } else {
 | |
|             // Elsewhere we just disable the button
 | |
|             mGoButton.setEnabled(enabled);
 | |
|             mGoButton.setFocusable(enabled);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * React to typing in the GO search button by refocusing to EditText. 
 | |
|      * Continue typing the query.
 | |
|      */
 | |
|     View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
 | |
|         public boolean onKey(View v, int keyCode, KeyEvent event) {
 | |
|             // guard against possible race conditions
 | |
|             if (mSearchable == null) {
 | |
|                 return false;
 | |
|             }
 | |
|             
 | |
|             if (!event.isSystem() && 
 | |
|                     (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
 | |
|                     (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
 | |
|                     (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
 | |
|                     (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
 | |
|                 // restore focus and give key to EditText ...
 | |
|                 if (mSearchAutoComplete.requestFocus()) {
 | |
|                     return mSearchAutoComplete.dispatchKeyEvent(event);
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             return false;
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * React to a click in the GO button by launching a search.
 | |
|      */
 | |
|     View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
 | |
|         public void onClick(View v) {
 | |
|             // guard against possible race conditions
 | |
|             if (mSearchable == null) {
 | |
|                 return;
 | |
|             }
 | |
|             launchQuerySearch();
 | |
|         }
 | |
|     };
 | |
|     
 | |
|     /**
 | |
|      * React to a click in the voice search button.
 | |
|      */
 | |
|     View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
 | |
|         public void onClick(View v) {
 | |
|             // guard against possible race conditions
 | |
|             if (mSearchable == null) {
 | |
|                 return;
 | |
|             }
 | |
|             SearchableInfo searchable = mSearchable;
 | |
|             try {
 | |
|                 if (searchable.getVoiceSearchLaunchWebSearch()) {
 | |
|                     Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
 | |
|                             searchable);
 | |
|                     getContext().startActivity(webSearchIntent);
 | |
|                 } else if (searchable.getVoiceSearchLaunchRecognizer()) {
 | |
|                     Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
 | |
|                             searchable);
 | |
|                     getContext().startActivity(appSearchIntent);
 | |
|                 }
 | |
|             } catch (ActivityNotFoundException e) {
 | |
|                 // Should not happen, since we check the availability of
 | |
|                 // voice search before showing the button. But just in case...
 | |
|                 Log.w(LOG_TAG, "Could not find voice search activity");
 | |
|             }
 | |
|             dismiss();
 | |
|          }
 | |
|     };
 | |
|     
 | |
|     /**
 | |
|      * Create and return an Intent that can launch the voice search activity for web search.
 | |
|      */
 | |
|     private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
 | |
|         Intent voiceIntent = new Intent(baseIntent);
 | |
|         ComponentName searchActivity = searchable.getSearchActivity();
 | |
|         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
 | |
|                 searchActivity == null ? null : searchActivity.flattenToShortString());
 | |
|         return voiceIntent;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Create and return an Intent that can launch the voice search activity, perform a specific
 | |
|      * voice transcription, and forward the results to the searchable activity.
 | |
|      * 
 | |
|      * @param baseIntent The voice app search intent to start from
 | |
|      * @return A completely-configured intent ready to send to the voice search activity
 | |
|      */
 | |
|     private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
 | |
|         ComponentName searchActivity = searchable.getSearchActivity();
 | |
|         
 | |
|         // create the necessary intent to set up a search-and-forward operation
 | |
|         // in the voice search system.   We have to keep the bundle separate,
 | |
|         // because it becomes immutable once it enters the PendingIntent
 | |
|         Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
 | |
|         queryIntent.setComponent(searchActivity);
 | |
|         PendingIntent pending = PendingIntent.getActivity(
 | |
|                 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
 | |
|         
 | |
|         // Now set up the bundle that will be inserted into the pending intent
 | |
|         // when it's time to do the search.  We always build it here (even if empty)
 | |
|         // because the voice search activity will always need to insert "QUERY" into
 | |
|         // it anyway.
 | |
|         Bundle queryExtras = new Bundle();
 | |
|         if (mAppSearchData != null) {
 | |
|             queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
 | |
|         }
 | |
|         
 | |
|         // Now build the intent to launch the voice search.  Add all necessary
 | |
|         // extras to launch the voice recognizer, and then all the necessary extras
 | |
|         // to forward the results to the searchable activity
 | |
|         Intent voiceIntent = new Intent(baseIntent);
 | |
|         
 | |
|         // Add all of the configuration options supplied by the searchable's metadata
 | |
|         String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
 | |
|         String prompt = null;
 | |
|         String language = null;
 | |
|         int maxResults = 1;
 | |
|         Resources resources = mActivityContext.getResources();
 | |
|         if (searchable.getVoiceLanguageModeId() != 0) {
 | |
|             languageModel = resources.getString(searchable.getVoiceLanguageModeId());
 | |
|         }
 | |
|         if (searchable.getVoicePromptTextId() != 0) {
 | |
|             prompt = resources.getString(searchable.getVoicePromptTextId());
 | |
|         }
 | |
|         if (searchable.getVoiceLanguageId() != 0) {
 | |
|             language = resources.getString(searchable.getVoiceLanguageId());
 | |
|         }
 | |
|         if (searchable.getVoiceMaxResults() != 0) {
 | |
|             maxResults = searchable.getVoiceMaxResults();
 | |
|         }
 | |
|         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
 | |
|         voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
 | |
|         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
 | |
|         voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
 | |
|         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
 | |
|                 searchActivity == null ? null : searchActivity.flattenToShortString());
 | |
|         
 | |
|         // Add the values that configure forwarding the results
 | |
|         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
 | |
|         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
 | |
|         
 | |
|         return voiceIntent;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Corrects http/https typo errors in the given url string, and if the protocol specifier was
 | |
|      * not present defaults to http.
 | |
|      * 
 | |
|      * @param inUrl URL to check and fix
 | |
|      * @return fixed URL string.
 | |
|      */
 | |
|     private String fixUrl(String inUrl) {
 | |
|         if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
 | |
|             return inUrl;
 | |
| 
 | |
|         if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) {
 | |
|             if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
 | |
|                 inUrl = inUrl.replaceFirst("/", "//");
 | |
|             } else {
 | |
|                 inUrl = inUrl.replaceFirst(":", "://");
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (inUrl.indexOf("://") == -1) {
 | |
|             inUrl = "http://" + inUrl;
 | |
|         }
 | |
| 
 | |
|         return inUrl;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * React to the user typing "enter" or other hardwired keys while typing in the search box.
 | |
|      * This handles these special keys while the edit box has focus.
 | |
|      */
 | |
|     View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
 | |
|         public boolean onKey(View v, int keyCode, KeyEvent event) {
 | |
|             // guard against possible race conditions
 | |
|             if (mSearchable == null) {
 | |
|                 return false;
 | |
|             }
 | |
| 
 | |
|             if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
 | |
|             if (DBG) { 
 | |
|                 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event 
 | |
|                         + "), selection: " + mSearchAutoComplete.getListSelection());
 | |
|             }
 | |
|             
 | |
|             // If a suggestion is selected, handle enter, search key, and action keys 
 | |
|             // as presses on the selected suggestion
 | |
|             if (mSearchAutoComplete.isPopupShowing() && 
 | |
|                     mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
 | |
|                 return onSuggestionsKey(v, keyCode, event);
 | |
|             }
 | |
| 
 | |
|             // If there is text in the query box, handle enter, and action keys
 | |
|             // The search key is handled by the dialog's onKeyDown(). 
 | |
|             if (!mSearchAutoComplete.isEmpty()) {
 | |
|                 if (keyCode == KeyEvent.KEYCODE_ENTER 
 | |
|                         && event.getAction() == KeyEvent.ACTION_UP) {
 | |
|                     v.cancelLongPress();
 | |
| 
 | |
|                     // If this is a url entered by the user & we displayed the 'Go' button which
 | |
|                     // the user clicked, launch the url instead of using it as a search query.
 | |
|                     if (mSearchable.autoUrlDetect() &&
 | |
|                         (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION)
 | |
|                                 == EditorInfo.IME_ACTION_GO) {
 | |
|                         Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString()));
 | |
|                         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
 | |
|                         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 | |
|                         launchIntent(intent);
 | |
|                     } else {
 | |
|                         // Launch as a regular search.
 | |
|                         launchQuerySearch();
 | |
|                     }
 | |
|                     return true;
 | |
|                 }
 | |
|                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
 | |
|                     SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
 | |
|                     if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
 | |
|                         launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
 | |
|                         return true;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             return false;
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     @Override
 | |
|     public void hide() {
 | |
|         if (!isShowing()) return;
 | |
| 
 | |
|         // We made sure the IME was displayed, so also make sure it is closed
 | |
|         // when we go away.
 | |
|         InputMethodManager imm = (InputMethodManager)getContext()
 | |
|                 .getSystemService(Context.INPUT_METHOD_SERVICE);
 | |
|         if (imm != null) {
 | |
|             imm.hideSoftInputFromWindow(
 | |
|                     getWindow().getDecorView().getWindowToken(), 0);
 | |
|         }
 | |
| 
 | |
|         super.hide();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * React to the user typing while in the suggestions list. First, check for action
 | |
|      * keys. If not handled, try refocusing regular characters into the EditText. 
 | |
|      */
 | |
|     private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
 | |
|         // guard against possible race conditions (late arrival after dismiss)
 | |
|         if (mSearchable == null) {
 | |
|             return false;
 | |
|         }
 | |
|         if (mSuggestionsAdapter == null) {
 | |
|             return false;
 | |
|         }
 | |
|         if (event.getAction() == KeyEvent.ACTION_DOWN) {
 | |
|             if (DBG_LOG_TIMING) {
 | |
|                 dbgLogTiming("onSuggestionsKey()");
 | |
|             }
 | |
|             
 | |
|             // First, check for enter or search (both of which we'll treat as a "click")
 | |
|             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
 | |
|                 int position = mSearchAutoComplete.getListSelection();
 | |
|                 return launchSuggestion(position);
 | |
|             }
 | |
|             
 | |
|             // Next, check for left/right moves, which we use to "return" the user to the edit view
 | |
|             if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
 | |
|                 // give "focus" to text editor, with cursor at the beginning if
 | |
|                 // left key, at end if right key
 | |
|                 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
 | |
|                 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 
 | |
|                         0 : mSearchAutoComplete.length();
 | |
|                 mSearchAutoComplete.setSelection(selPoint);
 | |
|                 mSearchAutoComplete.setListSelection(0);
 | |
|                 mSearchAutoComplete.clearListSelection();
 | |
|                 mSearchAutoComplete.ensureImeVisible(true);
 | |
|                 
 | |
|                 return true;
 | |
|             }
 | |
|             
 | |
|             // Next, check for an "up and out" move
 | |
|             if (keyCode == KeyEvent.KEYCODE_DPAD_UP 
 | |
|                     && 0 == mSearchAutoComplete.getListSelection()) {
 | |
|                 restoreUserQuery();
 | |
|                 // let ACTV complete the move
 | |
|                 return false;
 | |
|             }
 | |
|             
 | |
|             // Next, check for an "action key"
 | |
|             SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
 | |
|             if ((actionKey != null) && 
 | |
|                     ((actionKey.getSuggestActionMsg() != null) || 
 | |
|                      (actionKey.getSuggestActionMsgColumn() != null))) {
 | |
|                 // launch suggestion using action key column
 | |
|                 int position = mSearchAutoComplete.getListSelection();
 | |
|                 if (position != ListView.INVALID_POSITION) {
 | |
|                     Cursor c = mSuggestionsAdapter.getCursor();
 | |
|                     if (c.moveToPosition(position)) {
 | |
|                         final String actionMsg = getActionKeyMessage(c, actionKey);
 | |
|                         if (actionMsg != null && (actionMsg.length() > 0)) {
 | |
|                             return launchSuggestion(position, keyCode, actionMsg);
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Launch a search for the text in the query text field.
 | |
|      */
 | |
|     public void launchQuerySearch()  {
 | |
|         launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Launch a search for the text in the query text field.
 | |
|      *
 | |
|      * @param actionKey The key code of the action key that was pressed,
 | |
|      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
 | |
|      * @param actionMsg The message for the action key that was pressed,
 | |
|      *        or <code>null</code> if none.
 | |
|      */
 | |
|     protected void launchQuerySearch(int actionKey, String actionMsg)  {
 | |
|         String query = mSearchAutoComplete.getText().toString();
 | |
|         String action = Intent.ACTION_SEARCH;
 | |
|         Intent intent = createIntent(action, null, null, query, null,
 | |
|                 actionKey, actionMsg);
 | |
|         launchIntent(intent);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Launches an intent based on a suggestion.
 | |
|      * 
 | |
|      * @param position The index of the suggestion to create the intent from.
 | |
|      * @return true if a successful launch, false if could not (e.g. bad position).
 | |
|      */
 | |
|     protected boolean launchSuggestion(int position) {
 | |
|         return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Launches an intent based on a suggestion.
 | |
|      * 
 | |
|      * @param position The index of the suggestion to create the intent from.
 | |
|      * @param actionKey The key code of the action key that was pressed,
 | |
|      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
 | |
|      * @param actionMsg The message for the action key that was pressed,
 | |
|      *        or <code>null</code> if none.
 | |
|      * @return true if a successful launch, false if could not (e.g. bad position).
 | |
|      */
 | |
|     protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
 | |
|         Cursor c = mSuggestionsAdapter.getCursor();
 | |
|         if ((c != null) && c.moveToPosition(position)) {
 | |
| 
 | |
|             Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
 | |
| 
 | |
|            // launch the intent
 | |
|             launchIntent(intent);
 | |
| 
 | |
|             return true;
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Launches an intent, including any special intent handling.
 | |
|      */
 | |
|     private void launchIntent(Intent intent) {
 | |
|         if (intent == null) {
 | |
|             return;
 | |
|         }
 | |
|         Log.d(LOG_TAG, "launching " + intent);
 | |
|         try {
 | |
|             // If the intent was created from a suggestion, it will always have an explicit
 | |
|             // component here.
 | |
|             Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI());
 | |
|             getContext().startActivity(intent);
 | |
|             // If the search switches to a different activity,
 | |
|             // SearchDialogWrapper#performActivityResuming
 | |
|             // will handle hiding the dialog when the next activity starts, but for
 | |
|             // real in-app search, we still need to dismiss the dialog.
 | |
|             dismiss();
 | |
|         } catch (RuntimeException ex) {
 | |
|             Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * If the intent is to open an HTTP or HTTPS URL, we set
 | |
|      * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that
 | |
|      * has been opened by us for the same URL will be reused.
 | |
|      */
 | |
|     private void setBrowserApplicationId(Intent intent) {
 | |
|         Uri data = intent.getData();
 | |
|         if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) {
 | |
|             String scheme = data.getScheme();
 | |
|             if (scheme != null && scheme.startsWith("http")) {
 | |
|                 intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString());
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sets the list item selection in the AutoCompleteTextView's ListView.
 | |
|      */
 | |
|     public void setListSelection(int index) {
 | |
|         mSearchAutoComplete.setListSelection(index);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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.
 | |
|      * 
 | |
|      * @param c The suggestions cursor, moved to the row of the user's selection
 | |
|      * @param actionKey The key code of the action key that was pressed,
 | |
|      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
 | |
|      * @param actionMsg The message for the action key that was pressed,
 | |
|      *        or <code>null</code> if none.
 | |
|      * @return An intent for the suggestion at the cursor's position.
 | |
|      */
 | |
|     private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
 | |
|         try {
 | |
|             // use specific action if supplied, or default action if supplied, or fixed default
 | |
|             String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
 | |
| 
 | |
|             // some items are display only, or have effect via the cursor respond click reporting.
 | |
|             if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
 | |
|                 return null;
 | |
|             }
 | |
| 
 | |
|             if (action == null) {
 | |
|                 action = mSearchable.getSuggestIntentAction();
 | |
|             }
 | |
|             if (action == null) {
 | |
|                 action = Intent.ACTION_SEARCH;
 | |
|             }
 | |
|             
 | |
|             // use specific data if supplied, or default data if supplied
 | |
|             String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
 | |
|             if (data == null) {
 | |
|                 data = mSearchable.getSuggestIntentData();
 | |
|             }
 | |
|             // then, if an ID was provided, append it.
 | |
|             if (data != null) {
 | |
|                 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
 | |
|                 if (id != null) {
 | |
|                     data = data + "/" + Uri.encode(id);
 | |
|                 }
 | |
|             }
 | |
|             Uri dataUri = (data == null) ? null : Uri.parse(data);
 | |
| 
 | |
|             String componentName = getColumnString(
 | |
|                     c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
 | |
| 
 | |
|             String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
 | |
|             String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
 | |
| 
 | |
|             return createIntent(action, dataUri, extraData, query, componentName, actionKey,
 | |
|                     actionMsg);
 | |
|         } catch (RuntimeException e ) {
 | |
|             int rowNum;
 | |
|             try {                       // be really paranoid now
 | |
|                 rowNum = c.getPosition();
 | |
|             } catch (RuntimeException e2 ) {
 | |
|                 rowNum = -1;
 | |
|             }
 | |
|             Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + 
 | |
|                             " returned exception" + e.toString());
 | |
|             return null;
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Constructs an intent from the given information and the search dialog state.
 | |
|      * 
 | |
|      * @param action Intent action.
 | |
|      * @param data Intent data, or <code>null</code>.
 | |
|      * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
 | |
|      * @param query Intent query, or <code>null</code>.
 | |
|      * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
 | |
|      * @param actionKey The key code of the action key that was pressed,
 | |
|      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
 | |
|      * @param actionMsg The message for the action key that was pressed,
 | |
|      *        or <code>null</code> if none.
 | |
|      * @param mode The search mode, one of the acceptable values for
 | |
|      *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
 | |
|      * @return The intent.
 | |
|      */
 | |
|     private Intent createIntent(String action, Uri data, String extraData, String query,
 | |
|             String componentName, int actionKey, String actionMsg) {
 | |
|         // Now build the Intent
 | |
|         Intent intent = new Intent(action);
 | |
|         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 | |
|         // We need CLEAR_TOP to avoid reusing an old task that has other activities
 | |
|         // on top of the one we want. We don't want to do this in in-app search though,
 | |
|         // as it can be destructive to the activity stack.
 | |
|         if (data != null) {
 | |
|             intent.setData(data);
 | |
|         }
 | |
|         intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
 | |
|         if (query != null) {
 | |
|             intent.putExtra(SearchManager.QUERY, query);
 | |
|         }
 | |
|         if (extraData != null) {
 | |
|             intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
 | |
|         }
 | |
|         if (mAppSearchData != null) {
 | |
|             intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
 | |
|         }
 | |
|         if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
 | |
|             intent.putExtra(SearchManager.ACTION_KEY, actionKey);
 | |
|             intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
 | |
|         }
 | |
|         intent.setComponent(mSearchable.getSearchActivity());
 | |
|         return intent;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * For a given suggestion and a given cursor row, get the action message.  If not provided
 | |
|      * by the specific row/column, also check for a single definition (for the action key).
 | |
|      * 
 | |
|      * @param c The cursor providing suggestions
 | |
|      * @param actionKey The actionkey record being examined
 | |
|      * 
 | |
|      * @return Returns a string, or null if no action key message for this suggestion
 | |
|      */
 | |
|     private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
 | |
|         String result = null;
 | |
|         // check first in the cursor data, for a suggestion-specific message
 | |
|         final String column = actionKey.getSuggestActionMsgColumn();
 | |
|         if (column != null) {
 | |
|             result = SuggestionsAdapter.getColumnString(c, column);
 | |
|         }
 | |
|         // If the cursor didn't give us a message, see if there's a single message defined
 | |
|         // for the actionkey (for all suggestions)
 | |
|         if (result == null) {
 | |
|             result = actionKey.getSuggestActionMsg();
 | |
|         }
 | |
|         return result;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * The root element in the search bar layout. This is a custom view just to override
 | |
|      * the handling of the back button.
 | |
|      */
 | |
|     public static class SearchBar extends LinearLayout {
 | |
| 
 | |
|         private SearchDialog mSearchDialog;
 | |
| 
 | |
|         public SearchBar(Context context, AttributeSet attrs) {
 | |
|             super(context, attrs);
 | |
|         }
 | |
| 
 | |
|         public SearchBar(Context context) {
 | |
|             super(context);
 | |
|         }
 | |
| 
 | |
|         public void setSearchDialog(SearchDialog searchDialog) {
 | |
|             mSearchDialog = searchDialog;
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * Overrides the handling of the back key to move back to the previous sources or dismiss
 | |
|          * the search dialog, instead of dismissing the input method.
 | |
|          */
 | |
|         @Override
 | |
|         public boolean dispatchKeyEventPreIme(KeyEvent event) {
 | |
|             if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")");
 | |
|             if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
 | |
|                 KeyEvent.DispatcherState state = getKeyDispatcherState();
 | |
|                 if (state != null) {
 | |
|                     if (event.getAction() == KeyEvent.ACTION_DOWN
 | |
|                             && event.getRepeatCount() == 0) {
 | |
|                         state.startTracking(event, this);
 | |
|                         return true;
 | |
|                     } else if (event.getAction() == KeyEvent.ACTION_UP
 | |
|                             && !event.isCanceled() && state.isTracking(event)) {
 | |
|                         mSearchDialog.onBackPressed();
 | |
|                         return true;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             return super.dispatchKeyEventPreIme(event);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Local subclass for AutoCompleteTextView.
 | |
|      */
 | |
|     public static class SearchAutoComplete extends AutoCompleteTextView {
 | |
| 
 | |
|         private int mThreshold;
 | |
| 
 | |
|         public SearchAutoComplete(Context context) {
 | |
|             super(context);
 | |
|             mThreshold = getThreshold();
 | |
|         }
 | |
|         
 | |
|         public SearchAutoComplete(Context context, AttributeSet attrs) {
 | |
|             super(context, attrs);
 | |
|             mThreshold = getThreshold();
 | |
|         }
 | |
| 
 | |
|         public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
 | |
|             super(context, attrs, defStyle);
 | |
|             mThreshold = getThreshold();
 | |
|         }
 | |
| 
 | |
|         @Override
 | |
|         public void setThreshold(int threshold) {
 | |
|             super.setThreshold(threshold);
 | |
|             mThreshold = threshold;
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * Returns true if the text field is empty, or contains only whitespace.
 | |
|          */
 | |
|         private boolean isEmpty() {
 | |
|             return TextUtils.getTrimmedLength(getText()) == 0;
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * We override this method to avoid replacing the query box text
 | |
|          * when a suggestion is clicked.
 | |
|          */
 | |
|         @Override
 | |
|         protected void replaceText(CharSequence text) {
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * We override this method to avoid an extra onItemClick being called on the
 | |
|          * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)}
 | |
|          * when an item is clicked with the trackball.
 | |
|          */
 | |
|         @Override
 | |
|         public void performCompletion() {
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * We override this method to be sure and show the soft keyboard if appropriate when
 | |
|          * the TextView has focus.
 | |
|          */
 | |
|         @Override
 | |
|         public void onWindowFocusChanged(boolean hasWindowFocus) {
 | |
|             super.onWindowFocusChanged(hasWindowFocus);
 | |
| 
 | |
|             if (hasWindowFocus) {
 | |
|                 InputMethodManager inputManager = (InputMethodManager)
 | |
|                         getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
 | |
|                 inputManager.showSoftInput(this, 0);
 | |
|                 // If in landscape mode, then make sure that
 | |
|                 // the ime is in front of the dropdown.
 | |
|                 if (isLandscapeMode(getContext())) {
 | |
|                     ensureImeVisible(true);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|                 
 | |
|         /**
 | |
|          * We override this method so that we can allow a threshold of zero, which ACTV does not.
 | |
|          */
 | |
|         @Override
 | |
|         public boolean enoughToFilter() {
 | |
|             return mThreshold <= 0 || super.enoughToFilter();
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     @Override
 | |
|     public void onBackPressed() {
 | |
|         // If the input method is covering the search dialog completely,
 | |
|         // e.g. in landscape mode with no hard keyboard, dismiss just the input method
 | |
|         InputMethodManager imm = (InputMethodManager)getContext()
 | |
|                 .getSystemService(Context.INPUT_METHOD_SERVICE);
 | |
|         if (imm != null && imm.isFullscreenMode() &&
 | |
|                 imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) {
 | |
|             return;
 | |
|         }
 | |
|         // Close search dialog
 | |
|         cancel();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Implements OnItemClickListener
 | |
|      */
 | |
|     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
 | |
|         if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
 | |
|         launchSuggestion(position);
 | |
|     }
 | |
| 
 | |
|     /** 
 | |
|      * Implements OnItemSelectedListener
 | |
|      */
 | |
|      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
 | |
|          if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
 | |
|          // A suggestion has been selected, rewrite the query if possible,
 | |
|          // otherwise the restore the original query.
 | |
|          if (REWRITE_QUERIES) {
 | |
|              rewriteQueryFromSuggestion(position);
 | |
|          }
 | |
|      }
 | |
| 
 | |
|      /** 
 | |
|       * Implements OnItemSelectedListener
 | |
|       */
 | |
|      public void onNothingSelected(AdapterView<?> parent) {
 | |
|          if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
 | |
|      }
 | |
|      
 | |
|      /**
 | |
|       * Query rewriting.
 | |
|       */
 | |
| 
 | |
|      private void rewriteQueryFromSuggestion(int position) {
 | |
|          Cursor c = mSuggestionsAdapter.getCursor();
 | |
|          if (c == null) {
 | |
|              return;
 | |
|          }
 | |
|          if (c.moveToPosition(position)) {
 | |
|              // Get the new query from the suggestion.
 | |
|              CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
 | |
|              if (newQuery != null) {
 | |
|                  // The suggestion rewrites the query.
 | |
|                  if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
 | |
|                  // Update the text field, without getting new suggestions.
 | |
|                  setQuery(newQuery);
 | |
|              } else {
 | |
|                  // The suggestion does not rewrite the query, restore the user's query.
 | |
|                  if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
 | |
|                  restoreUserQuery();
 | |
|              }
 | |
|          } else {
 | |
|              // We got a bad position, restore the user's query.
 | |
|              Log.w(LOG_TAG, "Bad suggestion position: " + position);
 | |
|              restoreUserQuery();
 | |
|          }
 | |
|      }
 | |
|      
 | |
|      /** 
 | |
|       * Restores the query entered by the user if needed.
 | |
|       */
 | |
|      private void restoreUserQuery() {
 | |
|          if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
 | |
|          setQuery(mUserQuery);
 | |
|      }
 | |
|      
 | |
|      /**
 | |
|       * Sets the text in the query box, without updating the suggestions.
 | |
|       */
 | |
|      private void setQuery(CharSequence query) {
 | |
|          mSearchAutoComplete.setText(query, false);
 | |
|          if (query != null) {
 | |
|              mSearchAutoComplete.setSelection(query.length());
 | |
|          }
 | |
|      }
 | |
|      
 | |
|      /**
 | |
|       * Sets the text in the query box, updating the suggestions.
 | |
|       */
 | |
|      private void setUserQuery(String query) {
 | |
|          if (query == null) {
 | |
|              query = "";
 | |
|          }
 | |
|          mUserQuery = query;
 | |
|          mSearchAutoComplete.setText(query);
 | |
|          mSearchAutoComplete.setSelection(query.length());
 | |
|      }
 | |
| 
 | |
|     /**
 | |
|      * Debugging Support
 | |
|      */
 | |
| 
 | |
|     /**
 | |
|      * For debugging only, sample the millisecond clock and log it.
 | |
|      * Uses AtomicLong so we can use in multiple threads
 | |
|      */
 | |
|     private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
 | |
|     private void dbgLogTiming(final String caller) {
 | |
|         long millis = SystemClock.uptimeMillis();
 | |
|         long oldTime = mLastLogTime.getAndSet(millis);
 | |
|         long delta = millis - oldTime;
 | |
|         final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
 | |
|         Log.d(LOG_TAG,report);
 | |
|     }
 | |
| }
 | 
