Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3f703de0 authored by Amith Yamasani's avatar Amith Yamasani Committed by Android (Google) Code Review
Browse files

Merge "Add voice button and a bunch of minor fixes for focus and visibility of...

Merge "Add voice button and a bunch of minor fixes for focus and visibility of ime, submit button and drop down."
parents 3c036551 ebcf5a3a
Loading
Loading
Loading
Loading
+167 −9
Original line number Diff line number Diff line
@@ -20,15 +20,23 @@ import static android.widget.SuggestionsAdapter.getColumnString;

import com.android.internal.R;

import android.app.PendingIntent;
import android.app.SearchManager;
import android.app.SearchableInfo;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.speech.RecognizerIntent;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -66,6 +74,7 @@ public class SearchView extends LinearLayout {
    private View mSubmitButton;
    private View mCloseButton;
    private View mSearchEditFrame;
    private View mVoiceButton;
    private AutoCompleteTextView mQueryTextView;
    private boolean mSubmitButtonEnabled;
    private CharSequence mQueryHint;
@@ -74,6 +83,10 @@ public class SearchView extends LinearLayout {

    private SearchableInfo mSearchable;

    // For voice searching
    private final Intent mVoiceWebSearchIntent;
    private final Intent mVoiceAppSearchIntent;

    // 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 =
@@ -162,10 +175,13 @@ public class SearchView extends LinearLayout {
        mSearchEditFrame = findViewById(R.id.search_edit_frame);
        mSubmitButton = findViewById(R.id.search_go_btn);
        mCloseButton = findViewById(R.id.search_close_btn);
        mVoiceButton = findViewById(R.id.search_voice_btn);

        mSearchButton.setOnClickListener(mOnClickListener);
        mCloseButton.setOnClickListener(mOnClickListener);
        mSubmitButton.setOnClickListener(mOnClickListener);
        mVoiceButton.setOnClickListener(mOnClickListener);

        mQueryTextView.addTextChangedListener(mTextWatcher);
        mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
        mQueryTextView.setOnItemClickListener(mOnItemClickListener);
@@ -184,6 +200,15 @@ public class SearchView extends LinearLayout {
        setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
        a.recycle();

        // 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);

        updateViewsVisibility(mIconifiedByDefault);
    }

@@ -206,12 +231,8 @@ public class SearchView extends LinearLayout {
    /** @hide */
    @Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        if (mClearingFocus) return false;
        boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect);
        if (result && !isIconified()) {
            setImeVisibility(true);
        }
        return result;
        if (mClearingFocus || isIconified()) return false;
        return mQueryTextView.requestFocus(direction, previouslyFocusedRect);
    }

    /** @hide */
@@ -299,6 +320,7 @@ public class SearchView extends LinearLayout {
     * @param iconified whether the search field should be iconified by default
     */
    public void setIconifiedByDefault(boolean iconified) {
        if (mIconifiedByDefault == iconified) return;
        mIconifiedByDefault = iconified;
        updateViewsVisibility(iconified);
        setImeVisibility(!iconified);
@@ -349,8 +371,8 @@ public class SearchView extends LinearLayout {
     * button is not required.
     */
    public void setSubmitButtonEnabled(boolean enabled) {
        mSubmitButton.setVisibility(enabled ? VISIBLE : GONE);
        mSubmitButtonEnabled = enabled;
        updateViewsVisibility(isIconified());
    }

    /**
@@ -424,6 +446,7 @@ public class SearchView extends LinearLayout {
        mSearchButton.setVisibility(visCollapsed);
        mSubmitButton.setVisibility(mSubmitButtonEnabled && hasText ? visExpanded : GONE);
        mSearchEditFrame.setVisibility(visExpanded);
        updateVoiceButton(!hasText);
    }

    private void setImeVisibility(boolean visible) {
@@ -458,6 +481,8 @@ public class SearchView extends LinearLayout {
                onCloseClicked();
            } else if (v == mSubmitButton) {
                onSubmitQuery();
            } else if (v == mVoiceButton) {
                onVoiceClicked();
            }
        }
    };
@@ -525,6 +550,34 @@ public class SearchView extends LinearLayout {
        }
    }

    /**
     * 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 (mSearchable != null && mSearchable.getVoiceSearchEnabled() && empty
                && !isIconified()) {
            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);
    }

    private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {

        /**
@@ -542,9 +595,11 @@ public class SearchView extends LinearLayout {
        if (isSubmitButtonEnabled()) {
            mSubmitButton.setVisibility(hasText ? VISIBLE : GONE);
        }
        if (mOnQueryChangeListener != null)
        updateVoiceButton(!hasText);
        if (mOnQueryChangeListener != null) {
            mOnQueryChangeListener.onQueryTextChanged(newText.toString());
        }
    }

    private void onSubmitQuery() {
        CharSequence query = mQueryTextView.getText();
@@ -555,21 +610,27 @@ public class SearchView extends LinearLayout {
                    launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
                    setImeVisibility(false);
                }
                dismissSuggestions();
            }
        }
    }

    private void dismissSuggestions() {
        mQueryTextView.dismissDropDown();
    }

    private void onCloseClicked() {
        if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
            CharSequence text = mQueryTextView.getText();
            if (TextUtils.isEmpty(text)) {
                // query field already empty, hide the keyboard and remove focus
                mQueryTextView.clearFocus();
                clearFocus();
                setImeVisibility(false);
            } else {
                mQueryTextView.setText("");
            }
            updateViewsVisibility(mIconifiedByDefault);
            if (mIconifiedByDefault) setImeVisibility(false);
        }
    }

@@ -579,6 +640,29 @@ public class SearchView extends LinearLayout {
        setImeVisibility(true);
    }

    private void onVoiceClicked() {
        // 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");
        }
    }

    private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {

        /**
@@ -590,6 +674,7 @@ public class SearchView extends LinearLayout {
            if (mOnSuggestionListener == null
                    || !mOnSuggestionListener.onSuggestionClicked(position)) {
                launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
                dismissSuggestions();
            }
        }
    };
@@ -741,6 +826,79 @@ public class SearchView extends LinearLayout {
        return intent;
    }

    /**
     * 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();

        // 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 = 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;
    }

    /**
     * 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,
+7 −5
Original line number Diff line number Diff line
@@ -23,14 +23,14 @@
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:focusable="true"
    android:descendantFocusability="afterDescendants">
    >

    <!-- This is actually used for the badge icon *or* the badge label (or neither) -->
    <TextView
        android:id="@+id/search_badge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center_vertical"
        android:layout_marginBottom="2dip"
        android:drawablePadding="0dip"
        android:textAppearance="?android:attr/textAppearanceSmall"
@@ -41,7 +41,7 @@
    <ImageView
        android:id="@+id/search_button"
        android:layout_height="36dip"
        android:layout_width="36dip"
        android:layout_width="match_parent"
        android:layout_marginRight="7dip"
        android:layout_gravity="center_vertical"
        android:src="@android:drawable/ic_btn_search"
@@ -51,7 +51,7 @@
    <LinearLayout
        android:id="@+id/search_edit_frame"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_height="match_parent"
        android:visibility="gone"
        android:orientation="horizontal"
        android:background="?android:attr/editTextBackground">
@@ -70,6 +70,7 @@
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:layout_weight="1"
            android:layout_gravity="bottom"
            android:paddingLeft="8dip"
            android:paddingRight="6dip"
            android:drawablePadding="2dip"
@@ -86,6 +87,7 @@
            android:popupBackground="@android:drawable/search_dropdown_background"
        />

        <!-- TODO: Use the generic dialog close drawable -->
        <ImageView
            android:id="@+id/search_close_btn"
            android:layout_width="24dp"