Bug 778588 - Support direct voice input from the location bar r=mhaigh
authorJames Hugman <jhugman@mozilla.com>
Tue, 10 Feb 2015 13:46:30 -0500
changeset 242086 38a668c3efaa990ab0585749f21b4ccc76fe71cc
parent 242085 c70ba1f08a7fd1487e95d59f1d616f0f7dc558c8
child 242093 9ac2e8cd4faff33702ff849bea34e54db18c2f0b
push id634
push usermozilla@noorenberghe.ca
push dateTue, 10 Feb 2015 22:34:30 +0000
reviewersmhaigh
bugs778588
milestone38.0a1
Bug 778588 - Support direct voice input from the location bar r=mhaigh
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/resources/drawable-hdpi/ab_mic.png
mobile/android/base/resources/drawable-mdpi/ab_mic.png
mobile/android/base/resources/drawable-xhdpi/ab_mic.png
mobile/android/base/resources/drawable-xxhdpi/ab_mic.png
mobile/android/base/resources/layout/toolbar_edit_layout.xml
mobile/android/base/strings.xml.in
mobile/android/base/toolbar/ToolbarEditText.java
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -556,16 +556,23 @@ just addresses the organization to follo
 
 <!ENTITY colon ":">
 
 <!-- These are only used for accessibility for the done and overflow-menu buttons in the actionbar.
      They are never shown to users -->
 <!ENTITY actionbar_menu "Menu">
 <!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
      "relative time span string" produced by Android.  This string describes the
      time the tabs were last synced relative to the current time; examples
      include "42 minutes ago", "4 days ago", "last week", etc. The subject of
      "Last synced" is one of the user's other Sync clients, typically Firefox on
      their desktop or laptop.-->
 <!ENTITY remote_tabs_last_synced "Last synced: &formatS;">
 
new file mode 100644
index 0000000000000000000000000000000000000000..b74044e6c31723a909be286150e9972af515e487
GIT binary patch
literal 874
zc$@)h1C{)VP)<h;3K|Lk000e1NJLTq002Ay002A)1^@s6I{evk0009qNkl<ZXhZFo
zOK4L;6o#7@RS*{yy3mEKxO5prtfC=Fv7sxA_&~&hg6K*?QP9>_r3+CvqMM=x1s~8Z
z(=<&<#kPoa>&AuDg@S?+MbMC#_?<G5OYI|zJJWbG1H+v=narH;{AcFQjIW{2lCREq
z8!WKb6}Jjog+1m08`2&V^dB6nuvOS&9<U+pF+uI3Wi+HSnau8Tx!i->+m!34eLi1*
zEEaodDzc0L1A7SA$AASE!2JgLD9}$$KpO!Db_1~Q09wBU)OVn_0R2Tbv=QKRI(;0V
zhs(j>knZ;O_93qT_W<0t0&p+D{hk0GA0NL8Nbd?Teerl)+G$NjLfQf^u3>#C%pC7w
zmA<Amp6R@nLi!4G___sl*#lN#mmNCaU6rf!{}<BisP?e<L<F)>C=4VLiM*7lUvc9>
zv&+ok8`3_ROg4r>p-WO`2bN_+^1>}4Tn%ZIJd0ts#W=s;-x7^R-z(s1wwA{LdnD~A
zCMGtQN~Jjkc@9V1sYzfC3HwlFR2kQWp(9eR;_&b-pU<~;c6NSMQ(!;OVJQ<a9el1*
zH3^)}X4e&q#m^YGmOKJ*SH;lfj)$~4Lfhw+wnU1+3iQulFxVW4M1IsHuu|s%&u(&w
z+y=-i2ek77piKP?S$0@c4%i)(r)5^4?hxuqU#=xr=df5cHa6DE{Cy)TWAGe#7jyYT
zH5M~>H!&rq$StBS|J??#oTFV!t^<o@tjeb__P&%A^g*CsFK0Ed+W~#1VC8>jfW7A$
zsp?1zunO$~o)^_Pa*gwZm3OeIsc9x04i}_;c6K(D%jI?w5hnmWOjYIj=O$r(!EM-i
z0d#a)>63-(G$6NlU*i}@zRe%i`$nFPj^BP!J`e0&U3O^&7Nb+C)Yd>CAU7EY$iXE*
ziqsFQ6VqE=u-X-N8qQ+Y40s2MpQQfpH7)Ohg!Cx$^^;D}O5fTQUI?*ZUv1>j@3g@J
z>kcccuz{P()eh;s7$8~FB%h#3-BGowz$W1q8_W2jWq}QQz$$Fm;l_uv3LAI1;Q_6}
zh8=EvIIFO6mm40?Ds0%{#)q>C8+W<k0qaiTKk0xoOx<+AzW@LL07*qoM6N<$f)6K_
Ao&W#<
new file mode 100644
index 0000000000000000000000000000000000000000..789eafa87033495bcac195b1708b4c6582d3db63
GIT binary patch
literal 524
zc$@(Y0`vWeP)<h;3K|Lk000e1NJLTq001Ze001Zm1^@s6jQ+T70005gNkl<ZSV!%a
zKTE?v7{;4F#V#sl6T}Z7h`5N0Lr7Agpkp=#1vkZ|n~pl^q;7%*-8w3SLMR;y4lX+A
z7ZAbGC=N<v;&XC^QX_5eLOtaUUM|Vqd-uENeeW(xM)0Bt&X<vZyb+QLN(Ffp;Psa|
zD1?HwS}hlb;T~X?DbQ)7(I^%Qg?AwUecwL>$a)*hk>fZUq5wVvWT*}11%Sh%0LDom
z0Oa0;+yf9Wg!oSa#!yfdIm8hFH3`^_0zc&7?<okNH-Og>LWgdWZ^w+HC{MO+8?@a=
zJ%{eOZWi3yhiMlB@EU`R>AF5@nr4`EP{(n6kk98gam(nqWggN00f5u}0^GtxJFrzu
zTJk(^Ae+tZCtY=bU%~ei*f#p^_6v~Q5O=UaRaG6!vaXUQu2!ovQ52EzzQQH|NK&Dv
z!W3efSF_}dhUqY(-Z22~Y1=sJHCPcAViEIL#AQ!pv@<gx@vl@WQ<|oo;c=3@lZ`=Y
z<lhJ#{}#_n$i*8LxUVr;9h-jTa#=GBV*wzG_(t-LloH-Af*?3|UH5}$4sO8K$(!e4
z8b1lmaU~#6cvHziUVu*isfM`IWGw9xkVUmtAE}^JkXHd-f0={-UcpZRf2%3HtWTl<
O0000<MNUMnLSTXc#N$)|
new file mode 100644
index 0000000000000000000000000000000000000000..2277ff082b1fb38b2e349a13c79a8c0253f7eda7
GIT binary patch
literal 1149
zc%17D@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U~%zuaSW-5dppZBLpV{QZMKQx
z%55tuF382a%Uhr%)n&FM^YR6`FHtND<Dys{^~GWe827$#yu-Kjfn&d1j$!7l8A=Cv
zMK4x~iEmi+pi509x2AsP#Fq|leD0N4P0iPPd8X#Q{r&G>{!afR?B6RR!pOf=tgx3s
z#aDrWRl&1iN;fmf)PM7anLcV3mX<f8wWjV0xGlAI-*mbD=T*76Z=V)e%u&u>x9UzP
zpNynrBxk{PMsIfWwGnrM6&_#A_~IL?DbTx<Z9!4o!hnhxzIkzrn_hhXZNB?x=Q>6U
zE2}Djf^(6E%nI^ZnVFXPs%6`fW0(c*e&_7Eo3Zi1@)yTu>l{0mHDflzs*{Qd8*kY#
zu<1hh)82$|HLN>vSJYFZ;Kd=P3w_OJHZo0EsLrSI@+N!4A&D1H=bzec5P$rD62q^b
zpMHw5D4Yyn;BuJ6pefD{rOt^r{8$#~u+DC({<gK>wr|@OXY{P^cbS=RU4rE0ge%K~
zR@UBlwrfRfan8)gGng4)*mgy)6RnV9=$plG>*l#Cj=y1(|B0OI&$zBuuz#Y-bk&`c
z8Qv=_-Y_jxh;^H^@s^zns~DtLiTf}JTl8mMKXzf?KOJucE#4v?1%|l}*KL=n&ieeb
zruW9Q|BKY~O1<Sjp5OR&zbY_Hd2by(niOdH=e(Tau1}r}c5#{_)>g+E7ICi+J}N!w
zb+h-mSutPEPxtDOoj+?<*Y>Sj_nN-sd;F)Tj+eolVZq19vKcqJN_iHX;u0|ak#WuF
z51075R_^<f#@l{J6<1VLv|fK*+h``1`EECNg>_D#LX#}(48P^h1p#4SjdY*0?~KvA
zbo1uV&6A|q*xAKdnh%<6`PH(>EOq<go3AAMWcrw{#H^U6zqjqxu}?q$tn2IRJ71<H
zCnXgn#KDp$CA0QPIOF=`e%3i_9y5fBA8lB_Yg3`*C#ky|)P*`%%q%&^?3dR*dA&lx
zRJk6`SAn&)wZ$4Dzf6;vLwO2h7Mzkb{uCrK=g|DiamkNv+4?a2{gLTzB-Xt$^6UGF
zK5Eh*FZW3#eN$3PN&3L^I8)^D%jH^|HeR_hhaqHU>gJnlJ@wZaQqpJo*fU&d+0`zw
zl_5S%?VZ>()7=}_{7hT<NQ?cfq{Cjm8rIv)0a=#8d;vQr**jft&$DmvkIw(H>SVz=
z##KohL?S!h-Q-<f`ncpT4{P7!e-5#+vGu{hm%nEnTCqeooWVqGo`*^p^E0y@ui92F
z(DRY`zOkb&)N|qHrG>H%djmdKCvE>Dbns8VK*JJK)&&eIoD+%)A=GuN32ROp?qpC2
l<aS{65K?&hh%#zFV?RS<LhhdYLSV7N;OXk;vd$@?2>@JE^1A>4
new file mode 100644
index 0000000000000000000000000000000000000000..b59c2695f260e0c7500a840befb9af084a6ad7da
GIT binary patch
literal 1843
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
Q@#ixH0{roQJYO*TZv-lQ7ytkO
--- a/mobile/android/base/resources/layout/toolbar_edit_layout.xml
+++ b/mobile/android/base/resources/layout/toolbar_edit_layout.xml
@@ -11,11 +11,12 @@
           style="@style/UrlBar.Title"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:layout_weight="1.0"
           android:inputType="textUri|textNoSuggestions"
           android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
           android:selectAllOnFocus="true"
           android:contentDescription="@string/url_bar_default_text"
+          android:drawableRight="@drawable/ab_mic"
           gecko:autoUpdateTheme="false"/>
 
 </merge>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -479,15 +479,22 @@
   <string name="guest_browsing_notification_text">&guest_browsing_notification_text;</string>
 
   <string name="exit_guest_session_title">&exit_guest_session_title;</string>
   <string name="exit_guest_session_text">&exit_guest_session_text;</string>
 
   <string name="actionbar_menu">&actionbar_menu;</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 -->
   <string name="ellipsis">&ellipsis;</string>
 
   <string name="colon">&colon;</string>
 
   <string name="remote_tabs_last_synced">&remote_tabs_last_synced;</string>
 </resources>
--- a/mobile/android/base/toolbar/ToolbarEditText.java
+++ b/mobile/android/base/toolbar/ToolbarEditText.java
@@ -1,46 +1,60 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 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.CustomEditText;
+import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.InputMethods;
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
+import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.StringUtils;
 
+import android.app.Activity;
+import android.app.AlertDialog;
 import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.speech.RecognizerIntent;
 import android.text.Editable;
 import android.text.NoCopySpan;
 import android.text.Selection;
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.text.style.BackgroundColorSpan;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputConnectionWrapper;
 import android.view.inputmethod.InputMethodManager;
 import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.TextView;
 
+import java.util.List;
+
 /**
 * {@code ToolbarEditText} is the text entry used when the toolbar
 * is in edit state. It handles all the necessary input method machinery.
 * It's meant to be owned by {@code ToolbarEditLayout}.
 */
 public class ToolbarEditText extends CustomEditText
                              implements AutocompleteHandler {
 
@@ -84,16 +98,17 @@ public class ToolbarEditText extends Cus
     }
 
     @Override
     public void onAttachedToWindow() {
         setOnKeyListener(new KeyListener());
         setOnKeyPreImeListener(new KeyPreImeListener());
         setOnSelectionChangedListener(new SelectionChangeListener());
         addTextChangedListener(new TextChangeListener());
+        configureCompoundDrawables();
     }
 
     @Override
     public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
 
         if (gainFocus) {
             resetAutocompleteState();
@@ -450,16 +465,108 @@ public class ToolbarEditText extends Cus
                 if (removeAutocompleteOnComposing(text)) {
                     return false;
                 }
                 return super.setComposingText(text, newCursorPosition);
             }
         };
     }
 
+    /**
+     * 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 {
         @Override
         public void onSelectionChanged(final int selStart, final int selEnd) {
             // The user has repositioned the cursor somewhere. We need to adjust
             // the autocomplete text depending on where the new cursor is.
 
             final Editable text = getText();
             final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
@@ -593,9 +700,43 @@ public class ToolbarEditText extends Cus
                 removeAutocomplete(getText())) {
                 // Delete autocomplete text when backspacing or forward deleting.
                 return true;
             }
 
             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;
+        }
+    }
 }