blob: f93716e5a323cd5272078327c9945c7934f1d5ba [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quicksearchbox.ui;
import com.android.quicksearchbox.Corpora;
import com.android.quicksearchbox.Corpus;
import com.android.quicksearchbox.CorpusResult;
import com.android.quicksearchbox.Logger;
import com.android.quicksearchbox.Promoter;
import com.android.quicksearchbox.QsbApplication;
import com.android.quicksearchbox.R;
import com.android.quicksearchbox.SearchActivity;
import com.android.quicksearchbox.SuggestionCursor;
import com.android.quicksearchbox.Suggestions;
import com.android.quicksearchbox.VoiceSearch;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import java.util.ArrayList;
import java.util.Arrays;
public abstract class SearchActivityView extends RelativeLayout {
protected static final boolean DBG = false;
protected static final String TAG = "QSB.SearchActivityView";
// 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.
// TODO: This should move to android-common or something.
private static final String IME_OPTION_NO_MICROPHONE = "nm";
private Corpus mCorpus;
protected QueryTextView mQueryTextView;
// True if the query was empty on the previous call to updateQuery()
protected boolean mQueryWasEmpty = true;
protected Drawable mQueryTextEmptyBg;
protected Drawable mQueryTextNotEmptyBg;
protected SuggestionsListView<ListAdapter> mSuggestionsView;
protected SuggestionsAdapter<ListAdapter> mSuggestionsAdapter;
protected ImageButton mSearchCloseButton;
protected ImageButton mSearchGoButton;
protected ImageButton mVoiceSearchButton;
protected ButtonsKeyListener mButtonsKeyListener;
private boolean mUpdateSuggestions;
private QueryListener mQueryListener;
private SearchClickListener mSearchClickListener;
protected View.OnClickListener mExitClickListener;
public SearchActivityView(Context context) {
super(context);
}
public SearchActivityView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SearchActivityView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onFinishInflate() {
mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text);
mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
mSuggestionsView.setOnScrollListener(new InputMethodCloser());
mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener());
mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener());
mSuggestionsAdapter = createSuggestionsAdapter();
// TODO: why do we need focus listeners both on the SuggestionsView and the individual
// suggestions?
mSuggestionsAdapter.setOnFocusChangeListener(new SuggestListFocusListener());
mSearchCloseButton = (ImageButton) findViewById(R.id.search_close_btn);
mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
mVoiceSearchButton.setImageDrawable(getVoiceSearchIcon());
mQueryTextView.addTextChangedListener(new SearchTextWatcher());
mQueryTextView.setOnEditorActionListener(new QueryTextEditorActionListener());
mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
mQueryTextEmptyBg = mQueryTextView.getBackground();
mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
mButtonsKeyListener = new ButtonsKeyListener();
mSearchGoButton.setOnKeyListener(mButtonsKeyListener);
mVoiceSearchButton.setOnKeyListener(mButtonsKeyListener);
if (mSearchCloseButton != null) {
mSearchCloseButton.setOnKeyListener(mButtonsKeyListener);
mSearchCloseButton.setOnClickListener(new CloseClickListener());
}
mUpdateSuggestions = true;
}
public abstract void onResume();
public abstract void onStop();
public void onPause() {
// Override if necessary
}
public void start() {
mSuggestionsAdapter.getListAdapter().registerDataSetObserver(new SuggestionsObserver());
mSuggestionsView.setSuggestionsAdapter(mSuggestionsAdapter);
}
public void destroy() {
mSuggestionsView.setSuggestionsAdapter(null); // closes mSuggestionsAdapter
}
// TODO: Get rid of this. To make it more easily testable,
// the SearchActivityView should not depend on QsbApplication.
protected QsbApplication getQsbApplication() {
return QsbApplication.get(getContext());
}
protected Drawable getVoiceSearchIcon() {
return getResources().getDrawable(R.drawable.ic_btn_speak_now);
}
protected VoiceSearch getVoiceSearch() {
return getQsbApplication().getVoiceSearch();
}
protected SuggestionsAdapter<ListAdapter> createSuggestionsAdapter() {
return new DelayingSuggestionsAdapter<ListAdapter>(new SuggestionsListAdapter(
getQsbApplication().getSuggestionViewFactory()));
}
protected Corpora getCorpora() {
return getQsbApplication().getCorpora();
}
public Corpus getCorpus() {
return mCorpus;
}
protected abstract Promoter createSuggestionsPromoter();
protected Corpus getCorpus(String sourceName) {
if (sourceName == null) return null;
Corpus corpus = getCorpora().getCorpus(sourceName);
if (corpus == null) {
Log.w(TAG, "Unknown corpus " + sourceName);
return null;
}
return corpus;
}
public void onCorpusSelected(String corpusName) {
setCorpus(corpusName);
focusQueryTextView();
showInputMethodForQuery();
}
public void setCorpus(String corpusName) {
if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")");
Corpus corpus = getCorpus(corpusName);
setCorpus(corpus);
updateUi();
}
protected void setCorpus(Corpus corpus) {
mCorpus = corpus;
mSuggestionsAdapter.setPromoter(createSuggestionsPromoter());
Suggestions suggestions = getSuggestions();
if (corpus == null || suggestions == null || !suggestions.expectsCorpus(corpus)) {
getActivity().updateSuggestions();
}
}
public String getCorpusName() {
Corpus corpus = getCorpus();
return corpus == null ? null : corpus.getName();
}
public abstract Corpus getSearchCorpus();
public Corpus getWebCorpus() {
Corpus webCorpus = getCorpora().getWebCorpus();
if (webCorpus == null) {
Log.e(TAG, "No web corpus");
}
return webCorpus;
}
public void setMaxPromotedSuggestions(int maxPromoted) {
mSuggestionsView.setLimitSuggestionsToViewHeight(false);
mSuggestionsAdapter.setMaxPromoted(maxPromoted);
}
public void limitSuggestionsToViewHeight() {
mSuggestionsView.setLimitSuggestionsToViewHeight(true);
}
public void setMaxPromotedResults(int maxPromoted) {
}
public void limitResultsToViewHeight() {
}
public void setQueryListener(QueryListener listener) {
mQueryListener = listener;
}
public void setSearchClickListener(SearchClickListener listener) {
mSearchClickListener = listener;
}
public abstract void showCorpusSelectionDialog();
public void setVoiceSearchButtonClickListener(View.OnClickListener listener) {
if (mVoiceSearchButton != null) {
mVoiceSearchButton.setOnClickListener(listener);
}
}
public void setSuggestionClickListener(final SuggestionClickListener listener) {
mSuggestionsAdapter.setSuggestionClickListener(listener);
mQueryTextView.setCommitCompletionListener(new QueryTextView.CommitCompletionListener() {
@Override
public void onCommitCompletion(int position) {
mSuggestionsAdapter.onSuggestionClicked(position);
}
});
}
public void setExitClickListener(final View.OnClickListener listener) {
mExitClickListener = listener;
}
public Suggestions getSuggestions() {
return mSuggestionsAdapter.getSuggestions();
}
public SuggestionCursor getCurrentPromotedSuggestions() {
return mSuggestionsAdapter.getCurrentPromotedSuggestions();
}
public void setSuggestions(Suggestions suggestions) {
suggestions.acquire();
mSuggestionsAdapter.setSuggestions(suggestions);
}
public void clearSuggestions() {
mSuggestionsAdapter.setSuggestions(null);
}
public String getQuery() {
CharSequence q = mQueryTextView.getText();
return q == null ? "" : q.toString();
}
public boolean isQueryEmpty() {
return TextUtils.isEmpty(getQuery());
}
/**
* Sets the text in the query box. Does not update the suggestions.
*/
public void setQuery(String query, boolean selectAll) {
mUpdateSuggestions = false;
mQueryTextView.setText(query);
mQueryTextView.setTextSelection(selectAll);
mUpdateSuggestions = true;
}
protected SearchActivity getActivity() {
Context context = getContext();
if (context instanceof SearchActivity) {
return (SearchActivity) context;
} else {
return null;
}
}
public void hideSuggestions() {
mSuggestionsView.setVisibility(GONE);
}
public void showSuggestions() {
mSuggestionsView.setVisibility(VISIBLE);
}
public void focusQueryTextView() {
mQueryTextView.requestFocus();
}
protected void updateUi() {
updateUi(isQueryEmpty());
}
protected void updateUi(boolean queryEmpty) {
updateQueryTextView(queryEmpty);
updateSearchGoButton(queryEmpty);
updateVoiceSearchButton(queryEmpty);
}
protected void updateQueryTextView(boolean queryEmpty) {
if (queryEmpty) {
if (isSearchCorpusWeb()) {
mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg);
mQueryTextView.setHint(null);
} else {
if (mQueryTextNotEmptyBg == null) {
mQueryTextNotEmptyBg =
getResources().getDrawable(R.drawable.textfield_search_empty);
}
mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg);
Corpus corpus = getCorpus();
mQueryTextView.setHint(corpus == null ? "" : corpus.getHint());
}
} else {
mQueryTextView.setBackgroundResource(R.drawable.textfield_search);
}
}
private void updateSearchGoButton(boolean queryEmpty) {
if (queryEmpty) {
mSearchGoButton.setVisibility(View.GONE);
} else {
mSearchGoButton.setVisibility(View.VISIBLE);
}
}
protected void updateVoiceSearchButton(boolean queryEmpty) {
if (shouldShowVoiceSearch(queryEmpty)
&& getVoiceSearch().shouldShowVoiceSearch(getCorpus())) {
mVoiceSearchButton.setVisibility(View.VISIBLE);
mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
} else {
mVoiceSearchButton.setVisibility(View.GONE);
mQueryTextView.setPrivateImeOptions(null);
}
}
protected boolean shouldShowVoiceSearch(boolean queryEmpty) {
return queryEmpty;
}
/**
* Hides the input method.
*/
protected void hideInputMethod() {
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(getWindowToken(), 0);
}
}
public abstract void considerHidingInputMethod();
public void showInputMethodForQuery() {
mQueryTextView.showInputMethod();
}
/**
* Dismiss the activity if BACK is pressed when the search box is empty.
*/
@Override
public boolean dispatchKeyEventPreIme(KeyEvent event) {
SearchActivity activity = getActivity();
if (activity != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK
&& isQueryEmpty()) {
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)) {
hideInputMethod();
activity.onBackPressed();
return true;
}
}
}
return super.dispatchKeyEventPreIme(event);
}
/**
* If the input method is in fullscreen mode, and the selector corpus
* is All or Web, use the web search suggestions as completions.
*/
protected void updateInputMethodSuggestions() {
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null || !imm.isFullscreenMode()) return;
Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
if (suggestions == null) return;
CompletionInfo[] completions = webSuggestionsToCompletions(suggestions);
if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")");
imm.displayCompletions(mQueryTextView, completions);
}
private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) {
// TODO: This should also include include web search shortcuts
CorpusResult cursor = suggestions.getWebResult();
if (cursor == null) return null;
int count = cursor.getCount();
ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count);
boolean usingWebCorpus = isSearchCorpusWeb();
for (int i = 0; i < count; i++) {
cursor.moveTo(i);
if (!usingWebCorpus || cursor.isWebSearchSuggestion()) {
String text1 = cursor.getSuggestionText1();
completions.add(new CompletionInfo(i, i, text1));
}
}
return completions.toArray(new CompletionInfo[completions.size()]);
}
protected void onSuggestionsChanged() {
updateInputMethodSuggestions();
}
/**
* Checks if the corpus used for typed searches is the web corpus.
*/
protected boolean isSearchCorpusWeb() {
Corpus corpus = getSearchCorpus();
return corpus != null && corpus.isWebCorpus();
}
protected boolean onSuggestionKeyDown(SuggestionsAdapter<?> adapter,
long suggestionId, int keyCode, KeyEvent event) {
// Treat enter or search as a click
if ( keyCode == KeyEvent.KEYCODE_ENTER
|| keyCode == KeyEvent.KEYCODE_SEARCH
|| keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
if (adapter != null) {
adapter.onSuggestionClicked(suggestionId);
return true;
} else {
return false;
}
}
return false;
}
protected boolean onSearchClicked(int method) {
if (mSearchClickListener != null) {
return mSearchClickListener.onSearchClicked(method);
}
return false;
}
/**
* Filters the suggestions list when the search text changes.
*/
private class SearchTextWatcher implements TextWatcher {
public void afterTextChanged(Editable s) {
boolean empty = s.length() == 0;
if (empty != mQueryWasEmpty) {
mQueryWasEmpty = empty;
updateUi(empty);
}
if (mUpdateSuggestions) {
if (mQueryListener != null) {
mQueryListener.onQueryChanged();
}
}
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
}
/**
* Handles key events on the suggestions list view.
*/
protected class SuggestionsViewKeyListener implements View.OnKeyListener {
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN
&& v instanceof SuggestionsListView<?>) {
SuggestionsListView<?> listView = (SuggestionsListView<?>) v;
if (onSuggestionKeyDown(listView.getSuggestionsAdapter(),
listView.getSelectedItemId(), keyCode, event)) {
return true;
}
}
return forwardKeyToQueryTextView(keyCode, event);
}
}
private class InputMethodCloser implements SuggestionsView.OnScrollListener {
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
considerHidingInputMethod();
}
}
/**
* Listens for clicks on the source selector.
*/
private class SearchGoButtonClickListener implements View.OnClickListener {
public void onClick(View view) {
onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
}
}
/**
* This class handles enter key presses in the query text view.
*/
private class QueryTextEditorActionListener implements OnEditorActionListener {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
boolean consumed = false;
if (event != null) {
if (event.getAction() == KeyEvent.ACTION_UP) {
consumed = onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
} else if (event.getAction() == KeyEvent.ACTION_DOWN) {
// we have to consume the down event so that we receive the up event too
consumed = true;
}
}
if (DBG) Log.d(TAG, "onEditorAction consumed=" + consumed);
return consumed;
}
}
/**
* Handles key events on the search and voice search buttons,
* by refocusing to EditText.
*/
private class ButtonsKeyListener implements View.OnKeyListener {
public boolean onKey(View v, int keyCode, KeyEvent event) {
return forwardKeyToQueryTextView(keyCode, event);
}
}
private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
if (!event.isSystem() && shouldForwardToQueryTextView(keyCode)) {
if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
if (mQueryTextView.requestFocus()) {
return mQueryTextView.dispatchKeyEvent(event);
}
}
return false;
}
private boolean shouldForwardToQueryTextView(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_SEARCH:
return false;
default:
return true;
}
}
/**
* Hides the input method when the suggestions get focus.
*/
private class SuggestListFocusListener implements OnFocusChangeListener {
public void onFocusChange(View v, boolean focused) {
if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
if (focused) {
considerHidingInputMethod();
}
}
}
private class QueryTextViewFocusListener implements OnFocusChangeListener {
public void onFocusChange(View v, boolean focused) {
if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
if (focused) {
// The query box got focus, show the input method
showInputMethodForQuery();
}
}
}
protected class SuggestionsObserver extends DataSetObserver {
@Override
public void onChanged() {
onSuggestionsChanged();
}
}
public interface QueryListener {
void onQueryChanged();
}
public interface SearchClickListener {
boolean onSearchClicked(int method);
}
private class CloseClickListener implements OnClickListener {
public void onClick(View v) {
if (!isQueryEmpty()) {
mQueryTextView.setText("");
} else {
mExitClickListener.onClick(v);
}
}
}
}