Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.quicksearchbox.ui;
     18 
     19 import android.content.Context;
     20 import android.database.DataSetObserver;
     21 import android.graphics.drawable.Drawable;
     22 import android.text.Editable;
     23 import android.text.TextUtils;
     24 import android.text.TextWatcher;
     25 import android.util.AttributeSet;
     26 import android.util.Log;
     27 import android.view.KeyEvent;
     28 import android.view.View;
     29 import android.view.inputmethod.CompletionInfo;
     30 import android.view.inputmethod.InputMethodManager;
     31 import android.widget.AbsListView;
     32 import android.widget.ImageButton;
     33 import android.widget.ListAdapter;
     34 import android.widget.RelativeLayout;
     35 import android.widget.TextView;
     36 import android.widget.TextView.OnEditorActionListener;
     37 
     38 import com.android.quicksearchbox.Logger;
     39 import com.android.quicksearchbox.QsbApplication;
     40 import com.android.quicksearchbox.R;
     41 import com.android.quicksearchbox.SearchActivity;
     42 import com.android.quicksearchbox.SourceResult;
     43 import com.android.quicksearchbox.SuggestionCursor;
     44 import com.android.quicksearchbox.Suggestions;
     45 import com.android.quicksearchbox.VoiceSearch;
     46 
     47 import java.util.ArrayList;
     48 import java.util.Arrays;
     49 
     50 public abstract class SearchActivityView extends RelativeLayout {
     51     protected static final boolean DBG = false;
     52     protected static final String TAG = "QSB.SearchActivityView";
     53 
     54     // The string used for privateImeOptions to identify to the IME that it should not show
     55     // a microphone button since one already exists in the search dialog.
     56     // TODO: This should move to android-common or something.
     57     private static final String IME_OPTION_NO_MICROPHONE = "nm";
     58 
     59     protected QueryTextView mQueryTextView;
     60     // True if the query was empty on the previous call to updateQuery()
     61     protected boolean mQueryWasEmpty = true;
     62     protected Drawable mQueryTextEmptyBg;
     63     protected Drawable mQueryTextNotEmptyBg;
     64 
     65     protected SuggestionsListView<ListAdapter> mSuggestionsView;
     66     protected SuggestionsAdapter<ListAdapter> mSuggestionsAdapter;
     67 
     68     protected ImageButton mSearchGoButton;
     69     protected ImageButton mVoiceSearchButton;
     70 
     71     protected ButtonsKeyListener mButtonsKeyListener;
     72 
     73     private boolean mUpdateSuggestions;
     74 
     75     private QueryListener mQueryListener;
     76     private SearchClickListener mSearchClickListener;
     77     protected View.OnClickListener mExitClickListener;
     78 
     79     public SearchActivityView(Context context) {
     80         super(context);
     81     }
     82 
     83     public SearchActivityView(Context context, AttributeSet attrs) {
     84         super(context, attrs);
     85     }
     86 
     87     public SearchActivityView(Context context, AttributeSet attrs, int defStyle) {
     88         super(context, attrs, defStyle);
     89     }
     90 
     91     @Override
     92     protected void onFinishInflate() {
     93         mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text);
     94 
     95         mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
     96         mSuggestionsView.setOnScrollListener(new InputMethodCloser());
     97         mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener());
     98         mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener());
     99 
    100         mSuggestionsAdapter = createSuggestionsAdapter();
    101         // TODO: why do we need focus listeners both on the SuggestionsView and the individual
    102         // suggestions?
    103         mSuggestionsAdapter.setOnFocusChangeListener(new SuggestListFocusListener());
    104 
    105         mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
    106         mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
    107         mVoiceSearchButton.setImageDrawable(getVoiceSearchIcon());
    108 
    109         mQueryTextView.addTextChangedListener(new SearchTextWatcher());
    110         mQueryTextView.setOnEditorActionListener(new QueryTextEditorActionListener());
    111         mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
    112         mQueryTextEmptyBg = mQueryTextView.getBackground();
    113 
    114         mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
    115 
    116         mButtonsKeyListener = new ButtonsKeyListener();
    117         mSearchGoButton.setOnKeyListener(mButtonsKeyListener);
    118         mVoiceSearchButton.setOnKeyListener(mButtonsKeyListener);
    119 
    120         mUpdateSuggestions = true;
    121     }
    122 
    123     public abstract void onResume();
    124 
    125     public abstract void onStop();
    126 
    127     public void onPause() {
    128         // Override if necessary
    129     }
    130 
    131     public void start() {
    132         mSuggestionsAdapter.getListAdapter().registerDataSetObserver(new SuggestionsObserver());
    133         mSuggestionsView.setSuggestionsAdapter(mSuggestionsAdapter);
    134     }
    135 
    136     public void destroy() {
    137         mSuggestionsView.setSuggestionsAdapter(null);  // closes mSuggestionsAdapter
    138     }
    139 
    140     // TODO: Get rid of this. To make it more easily testable,
    141     // the SearchActivityView should not depend on QsbApplication.
    142     protected QsbApplication getQsbApplication() {
    143         return QsbApplication.get(getContext());
    144     }
    145 
    146     protected Drawable getVoiceSearchIcon() {
    147         return getResources().getDrawable(R.drawable.ic_btn_speak_now);
    148     }
    149 
    150     protected VoiceSearch getVoiceSearch() {
    151         return getQsbApplication().getVoiceSearch();
    152     }
    153 
    154     protected SuggestionsAdapter<ListAdapter> createSuggestionsAdapter() {
    155         return new DelayingSuggestionsAdapter<ListAdapter>(new SuggestionsListAdapter(
    156                 getQsbApplication().getSuggestionViewFactory()));
    157     }
    158 
    159     public void setMaxPromotedResults(int maxPromoted) {
    160     }
    161 
    162     public void limitResultsToViewHeight() {
    163     }
    164 
    165     public void setQueryListener(QueryListener listener) {
    166         mQueryListener = listener;
    167     }
    168 
    169     public void setSearchClickListener(SearchClickListener listener) {
    170         mSearchClickListener = listener;
    171     }
    172 
    173     public void setVoiceSearchButtonClickListener(View.OnClickListener listener) {
    174         if (mVoiceSearchButton != null) {
    175             mVoiceSearchButton.setOnClickListener(listener);
    176         }
    177     }
    178 
    179     public void setSuggestionClickListener(final SuggestionClickListener listener) {
    180         mSuggestionsAdapter.setSuggestionClickListener(listener);
    181         mQueryTextView.setCommitCompletionListener(new QueryTextView.CommitCompletionListener() {
    182             @Override
    183             public void onCommitCompletion(int position) {
    184                 mSuggestionsAdapter.onSuggestionClicked(position);
    185             }
    186         });
    187     }
    188 
    189     public void setExitClickListener(final View.OnClickListener listener) {
    190         mExitClickListener = listener;
    191     }
    192 
    193     public Suggestions getSuggestions() {
    194         return mSuggestionsAdapter.getSuggestions();
    195     }
    196 
    197     public SuggestionCursor getCurrentSuggestions() {
    198         return mSuggestionsAdapter.getSuggestions().getResult();
    199     }
    200 
    201     public void setSuggestions(Suggestions suggestions) {
    202         suggestions.acquire();
    203         mSuggestionsAdapter.setSuggestions(suggestions);
    204     }
    205 
    206     public void clearSuggestions() {
    207         mSuggestionsAdapter.setSuggestions(null);
    208     }
    209 
    210     public String getQuery() {
    211         CharSequence q = mQueryTextView.getText();
    212         return q == null ? "" : q.toString();
    213     }
    214 
    215     public boolean isQueryEmpty() {
    216         return TextUtils.isEmpty(getQuery());
    217     }
    218 
    219     /**
    220      * Sets the text in the query box. Does not update the suggestions.
    221      */
    222     public void setQuery(String query, boolean selectAll) {
    223         mUpdateSuggestions = false;
    224         mQueryTextView.setText(query);
    225         mQueryTextView.setTextSelection(selectAll);
    226         mUpdateSuggestions = true;
    227     }
    228 
    229     protected SearchActivity getActivity() {
    230         Context context = getContext();
    231         if (context instanceof SearchActivity) {
    232             return (SearchActivity) context;
    233         } else {
    234             return null;
    235         }
    236     }
    237 
    238     public void hideSuggestions() {
    239         mSuggestionsView.setVisibility(GONE);
    240     }
    241 
    242     public void showSuggestions() {
    243         mSuggestionsView.setVisibility(VISIBLE);
    244     }
    245 
    246     public void focusQueryTextView() {
    247         mQueryTextView.requestFocus();
    248     }
    249 
    250     protected void updateUi() {
    251         updateUi(isQueryEmpty());
    252     }
    253 
    254     protected void updateUi(boolean queryEmpty) {
    255         updateQueryTextView(queryEmpty);
    256         updateSearchGoButton(queryEmpty);
    257         updateVoiceSearchButton(queryEmpty);
    258     }
    259 
    260     protected void updateQueryTextView(boolean queryEmpty) {
    261         if (queryEmpty) {
    262             mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg);
    263             mQueryTextView.setHint(null);
    264         } else {
    265             mQueryTextView.setBackgroundResource(R.drawable.textfield_search);
    266         }
    267     }
    268 
    269     private void updateSearchGoButton(boolean queryEmpty) {
    270         if (queryEmpty) {
    271             mSearchGoButton.setVisibility(View.GONE);
    272         } else {
    273             mSearchGoButton.setVisibility(View.VISIBLE);
    274         }
    275     }
    276 
    277     protected void updateVoiceSearchButton(boolean queryEmpty) {
    278         if (shouldShowVoiceSearch(queryEmpty)
    279                 && getVoiceSearch().shouldShowVoiceSearch()) {
    280             mVoiceSearchButton.setVisibility(View.VISIBLE);
    281             mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
    282         } else {
    283             mVoiceSearchButton.setVisibility(View.GONE);
    284             mQueryTextView.setPrivateImeOptions(null);
    285         }
    286     }
    287 
    288     protected boolean shouldShowVoiceSearch(boolean queryEmpty) {
    289         return queryEmpty;
    290     }
    291 
    292     /**
    293      * Hides the input method.
    294      */
    295     protected void hideInputMethod() {
    296         InputMethodManager imm = (InputMethodManager)
    297                 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    298         if (imm != null) {
    299             imm.hideSoftInputFromWindow(getWindowToken(), 0);
    300         }
    301     }
    302 
    303     public abstract void considerHidingInputMethod();
    304 
    305     public void showInputMethodForQuery() {
    306         mQueryTextView.showInputMethod();
    307     }
    308 
    309     /**
    310      * Dismiss the activity if BACK is pressed when the search box is empty.
    311      */
    312     @Override
    313     public boolean dispatchKeyEventPreIme(KeyEvent event) {
    314         SearchActivity activity = getActivity();
    315         if (activity != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK
    316                 && isQueryEmpty()) {
    317             KeyEvent.DispatcherState state = getKeyDispatcherState();
    318             if (state != null) {
    319                 if (event.getAction() == KeyEvent.ACTION_DOWN
    320                         && event.getRepeatCount() == 0) {
    321                     state.startTracking(event, this);
    322                     return true;
    323                 } else if (event.getAction() == KeyEvent.ACTION_UP
    324                         && !event.isCanceled() && state.isTracking(event)) {
    325                     hideInputMethod();
    326                     activity.onBackPressed();
    327                     return true;
    328                 }
    329             }
    330         }
    331         return super.dispatchKeyEventPreIme(event);
    332     }
    333 
    334     /**
    335      * If the input method is in fullscreen mode, and the selector corpus
    336      * is All or Web, use the web search suggestions as completions.
    337      */
    338     protected void updateInputMethodSuggestions() {
    339         InputMethodManager imm = (InputMethodManager)
    340                 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    341         if (imm == null || !imm.isFullscreenMode()) return;
    342         Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
    343         if (suggestions == null) return;
    344         CompletionInfo[] completions = webSuggestionsToCompletions(suggestions);
    345         if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")");
    346         imm.displayCompletions(mQueryTextView, completions);
    347     }
    348 
    349     private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) {
    350         SourceResult cursor = suggestions.getWebResult();
    351         if (cursor == null) return null;
    352         int count = cursor.getCount();
    353         ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count);
    354         for (int i = 0; i < count; i++) {
    355             cursor.moveTo(i);
    356             String text1 = cursor.getSuggestionText1();
    357             completions.add(new CompletionInfo(i, i, text1));
    358         }
    359         return completions.toArray(new CompletionInfo[completions.size()]);
    360     }
    361 
    362     protected void onSuggestionsChanged() {
    363         updateInputMethodSuggestions();
    364     }
    365 
    366     protected boolean onSuggestionKeyDown(SuggestionsAdapter<?> adapter,
    367             long suggestionId, int keyCode, KeyEvent event) {
    368         // Treat enter or search as a click
    369         if (       keyCode == KeyEvent.KEYCODE_ENTER
    370                 || keyCode == KeyEvent.KEYCODE_SEARCH
    371                 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
    372             if (adapter != null) {
    373                 adapter.onSuggestionClicked(suggestionId);
    374                 return true;
    375             } else {
    376                 return false;
    377             }
    378         }
    379 
    380         return false;
    381     }
    382 
    383     protected boolean onSearchClicked(int method) {
    384         if (mSearchClickListener != null) {
    385             return mSearchClickListener.onSearchClicked(method);
    386         }
    387         return false;
    388     }
    389 
    390     /**
    391      * Filters the suggestions list when the search text changes.
    392      */
    393     private class SearchTextWatcher implements TextWatcher {
    394         @Override
    395         public void afterTextChanged(Editable s) {
    396             boolean empty = s.length() == 0;
    397             if (empty != mQueryWasEmpty) {
    398                 mQueryWasEmpty = empty;
    399                 updateUi(empty);
    400             }
    401             if (mUpdateSuggestions) {
    402                 if (mQueryListener != null) {
    403                     mQueryListener.onQueryChanged();
    404                 }
    405             }
    406         }
    407 
    408         @Override
    409         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    410         }
    411 
    412         @Override
    413         public void onTextChanged(CharSequence s, int start, int before, int count) {
    414         }
    415     }
    416 
    417     /**
    418      * Handles key events on the suggestions list view.
    419      */
    420     protected class SuggestionsViewKeyListener implements View.OnKeyListener {
    421         @Override
    422         public boolean onKey(View v, int keyCode, KeyEvent event) {
    423             if (event.getAction() == KeyEvent.ACTION_DOWN
    424                     && v instanceof SuggestionsListView<?>) {
    425                 SuggestionsListView<?> listView = (SuggestionsListView<?>) v;
    426                 if (onSuggestionKeyDown(listView.getSuggestionsAdapter(),
    427                         listView.getSelectedItemId(), keyCode, event)) {
    428                     return true;
    429                 }
    430             }
    431             return forwardKeyToQueryTextView(keyCode, event);
    432         }
    433     }
    434 
    435     private class InputMethodCloser implements SuggestionsView.OnScrollListener {
    436 
    437         @Override
    438         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    439                 int totalItemCount) {
    440         }
    441 
    442         @Override
    443         public void onScrollStateChanged(AbsListView view, int scrollState) {
    444             considerHidingInputMethod();
    445         }
    446     }
    447 
    448     /**
    449      * Listens for clicks on the source selector.
    450      */
    451     private class SearchGoButtonClickListener implements View.OnClickListener {
    452         @Override
    453         public void onClick(View view) {
    454             onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
    455         }
    456     }
    457 
    458     /**
    459      * This class handles enter key presses in the query text view.
    460      */
    461     private class QueryTextEditorActionListener implements OnEditorActionListener {
    462         @Override
    463         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    464             boolean consumed = false;
    465             if (event != null) {
    466                 if (event.getAction() == KeyEvent.ACTION_UP) {
    467                     consumed = onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
    468                 } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
    469                     // we have to consume the down event so that we receive the up event too
    470                     consumed = true;
    471                 }
    472             }
    473             if (DBG) Log.d(TAG, "onEditorAction consumed=" + consumed);
    474             return consumed;
    475         }
    476     }
    477 
    478     /**
    479      * Handles key events on the search and voice search buttons,
    480      * by refocusing to EditText.
    481      */
    482     private class ButtonsKeyListener implements View.OnKeyListener {
    483         @Override
    484         public boolean onKey(View v, int keyCode, KeyEvent event) {
    485             return forwardKeyToQueryTextView(keyCode, event);
    486         }
    487     }
    488 
    489     private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
    490         if (!event.isSystem() && shouldForwardToQueryTextView(keyCode)) {
    491             if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
    492             if (mQueryTextView.requestFocus()) {
    493                 return mQueryTextView.dispatchKeyEvent(event);
    494             }
    495         }
    496         return false;
    497     }
    498 
    499     private boolean shouldForwardToQueryTextView(int keyCode) {
    500         switch (keyCode) {
    501             case KeyEvent.KEYCODE_DPAD_UP:
    502             case KeyEvent.KEYCODE_DPAD_DOWN:
    503             case KeyEvent.KEYCODE_DPAD_LEFT:
    504             case KeyEvent.KEYCODE_DPAD_RIGHT:
    505             case KeyEvent.KEYCODE_DPAD_CENTER:
    506             case KeyEvent.KEYCODE_ENTER:
    507             case KeyEvent.KEYCODE_SEARCH:
    508                 return false;
    509             default:
    510                 return true;
    511         }
    512     }
    513 
    514     /**
    515      * Hides the input method when the suggestions get focus.
    516      */
    517     private class SuggestListFocusListener implements OnFocusChangeListener {
    518         @Override
    519         public void onFocusChange(View v, boolean focused) {
    520             if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
    521             if (focused) {
    522                 considerHidingInputMethod();
    523             }
    524         }
    525     }
    526 
    527     private class QueryTextViewFocusListener implements OnFocusChangeListener {
    528         @Override
    529         public void onFocusChange(View v, boolean focused) {
    530             if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
    531             if (focused) {
    532                 // The query box got focus, show the input method
    533                 showInputMethodForQuery();
    534             }
    535         }
    536     }
    537 
    538     protected class SuggestionsObserver extends DataSetObserver {
    539         @Override
    540         public void onChanged() {
    541             onSuggestionsChanged();
    542         }
    543     }
    544 
    545     public interface QueryListener {
    546         void onQueryChanged();
    547     }
    548 
    549     public interface SearchClickListener {
    550         boolean onSearchClicked(int method);
    551     }
    552 
    553     private class CloseClickListener implements OnClickListener {
    554         @Override
    555         public void onClick(View v) {
    556             if (!isQueryEmpty()) {
    557                 mQueryTextView.setText("");
    558             } else {
    559                 mExitClickListener.onClick(v);
    560             }
    561         }
    562     }
    563 }
    564