Bug 778588 - Support direct voice input from the location bar r=mhaigh

This commit is contained in:
James Hugman
2015-02-10 13:46:30 -05:00
parent 567b52e246
commit 3b0acad765
8 changed files with 156 additions and 0 deletions

View File

@@ -561,6 +561,13 @@ just addresses the organization to follow, e.g. "This site is run by " -->
<!ENTITY actionbar_menu "Menu"> <!ENTITY actionbar_menu "Menu">
<!ENTITY actionbar_done "Done"> <!ENTITY actionbar_done "Done">
<!-- Voice search in the awesome bar -->
<!ENTITY voicesearch_prompt "Speak now">
<!ENTITY voicesearch_failed_title "&brandShortName; Voice Search">
<!ENTITY voicesearch_failed_message "There is a problem with voice search right now. Please try later.">
<!ENTITY voicesearch_failed_message_recoverable "Sorry! We could not recognize your words. Please try again.">
<!ENTITY voicesearch_failed_retry "Try again">
<!-- Localization note (remote_tabs_last_synced): the variable is replaced by a <!-- Localization note (remote_tabs_last_synced): the variable is replaced by a
"relative time span string" produced by Android. This string describes the "relative time span string" produced by Android. This string describes the
time the tabs were last synced relative to the current time; examples time the tabs were last synced relative to the current time; examples

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -16,6 +16,7 @@
android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen" android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:contentDescription="@string/url_bar_default_text" android:contentDescription="@string/url_bar_default_text"
android:drawableRight="@drawable/ab_mic"
gecko:autoUpdateTheme="false"/> gecko:autoUpdateTheme="false"/>
</merge> </merge>

View File

@@ -484,6 +484,13 @@
<string name="actionbar_menu">&actionbar_menu;</string> <string name="actionbar_menu">&actionbar_menu;</string>
<string name="actionbar_done">&actionbar_done;</string> <string name="actionbar_done">&actionbar_done;</string>
<!-- Voice search from the Awesome Bar -->
<string name="voicesearch_prompt">&voicesearch_prompt;</string>
<string name="voicesearch_failed_title">&voicesearch_failed_title;</string>
<string name="voicesearch_failed_message">&voicesearch_failed_message;</string>
<string name="voicesearch_failed_message_recoverable">&voicesearch_failed_message_recoverable;</string>
<string name="voicesearch_failed_retry">&voicesearch_failed_retry;</string>
<!-- Miscellaneous --> <!-- Miscellaneous -->
<string name="ellipsis">&ellipsis;</string> <string name="ellipsis">&ellipsis;</string>

View File

@@ -5,17 +5,28 @@
package org.mozilla.gecko.toolbar; package org.mozilla.gecko.toolbar;
import org.mozilla.gecko.ActivityHandlerHelper;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.CustomEditText; import org.mozilla.gecko.CustomEditText;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.InputMethods; import org.mozilla.gecko.InputMethods;
import org.mozilla.gecko.R;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener; import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.StringUtils; import org.mozilla.gecko.util.StringUtils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.speech.RecognizerIntent;
import android.text.Editable; import android.text.Editable;
import android.text.NoCopySpan; import android.text.NoCopySpan;
import android.text.Selection; import android.text.Selection;
@@ -26,6 +37,7 @@ import android.text.style.BackgroundColorSpan;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
@@ -36,6 +48,8 @@ import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEvent;
import android.widget.TextView; import android.widget.TextView;
import java.util.List;
/** /**
* {@code ToolbarEditText} is the text entry used when the toolbar * {@code ToolbarEditText} is the text entry used when the toolbar
* is in edit state. It handles all the necessary input method machinery. * is in edit state. It handles all the necessary input method machinery.
@@ -89,6 +103,7 @@ public class ToolbarEditText extends CustomEditText
setOnKeyPreImeListener(new KeyPreImeListener()); setOnKeyPreImeListener(new KeyPreImeListener());
setOnSelectionChangedListener(new SelectionChangeListener()); setOnSelectionChangedListener(new SelectionChangeListener());
addTextChangedListener(new TextChangeListener()); addTextChangedListener(new TextChangeListener());
configureCompoundDrawables();
} }
@Override @Override
@@ -455,6 +470,98 @@ public class ToolbarEditText extends CustomEditText
}; };
} }
/**
* Detect if we are able to enable the 'buttons' made from compound drawables.
*
* Currently, only voice input.
*/
private void configureCompoundDrawables() {
if (!AppConstants.NIGHTLY_BUILD || !supportsVoiceRecognizer()) {
// Remove the mic button if we can't support the voice recognizer.
setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
return;
}
setOnTouchListener(new VoiceSearchOnTouchListener());
}
private boolean supportsVoiceRecognizer() {
final Intent intent = createVoiceRecognizerIntent();
return intent.resolveActivity(getContext().getPackageManager()) != null;
}
private Intent createVoiceRecognizerIntent() {
final Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, getResources().getString(R.string.voicesearch_prompt));
return intent;
}
private void launchVoiceRecognizer() {
final Intent intent = createVoiceRecognizerIntent();
Activity activity = GeckoAppShell.getGeckoInterface().getActivity();
ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() {
@Override
public void onActivityResult(int resultCode, Intent data) {
switch (resultCode) {
case RecognizerIntent.RESULT_CLIENT_ERROR:
case RecognizerIntent.RESULT_NETWORK_ERROR:
case RecognizerIntent.RESULT_SERVER_ERROR:
// We have an temporarily unrecoverable error.
handleVoiceSearchError(false);
break;
case RecognizerIntent.RESULT_AUDIO_ERROR:
case RecognizerIntent.RESULT_NO_MATCH:
// Maybe the user can say it differently?
handleVoiceSearchError(true);
break;
case Activity.RESULT_CANCELED:
break;
}
if (resultCode != Activity.RESULT_OK) {
return;
}
// We have RESULT_OK, not RESULT_NO_MATCH so it should be safe to assume that
// we have at least one match. We only need one: this will be
// used for showing the user search engines with this search term in it.
List<String> voiceStrings = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
String text = voiceStrings.get(0);
setText(text);
setSelection(0, text.length());
}
});
}
private void handleVoiceSearchError(boolean offerRetry) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext())
.setTitle(R.string.voicesearch_failed_title)
.setIcon(R.drawable.icon).setNeutralButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
if (offerRetry) {
builder.setMessage(R.string.voicesearch_failed_message_recoverable)
.setNegativeButton(R.string.voicesearch_failed_retry, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
launchVoiceRecognizer();
}
});
} else {
builder.setMessage(R.string.voicesearch_failed_message);
}
AlertDialog dialog = builder.create();
dialog.show();
}
private class SelectionChangeListener implements OnSelectionChangedListener { private class SelectionChangeListener implements OnSelectionChangedListener {
@Override @Override
public void onSelectionChanged(final int selStart, final int selEnd) { public void onSelectionChanged(final int selStart, final int selEnd) {
@@ -598,4 +705,38 @@ public class ToolbarEditText extends CustomEditText
return false; return false;
} }
} }
private class VoiceSearchOnTouchListener implements View.OnTouchListener {
private int mVoiceSearchIconIndex = -1;
private Drawable mVoiceSearchIcon;
public VoiceSearchOnTouchListener() {
Drawable[] drawables = getCompoundDrawables();
for (int i = 0; i < drawables.length; i++) {
if (drawables[i] != null) {
mVoiceSearchIcon = drawables[i];
mVoiceSearchIconIndex = i;
}
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
boolean tapped;
switch (mVoiceSearchIconIndex) {
case 0:
tapped = event.getX() < (getPaddingLeft() + mVoiceSearchIcon.getIntrinsicWidth());
break;
case 2:
tapped = event.getX() > (getWidth() - getPaddingRight() - mVoiceSearchIcon.getIntrinsicWidth());
break;
default:
tapped = false;
}
if (tapped) {
launchVoiceRecognizer();
}
return tapped;
}
}
} }