• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.inputmethod.deprecated;
18 
19 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
20 import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
21 import com.android.inputmethod.compat.SharedPreferencesCompat;
22 import com.android.inputmethod.deprecated.voice.FieldContext;
23 import com.android.inputmethod.deprecated.voice.Hints;
24 import com.android.inputmethod.deprecated.voice.SettingsUtil;
25 import com.android.inputmethod.deprecated.voice.VoiceInput;
26 import com.android.inputmethod.keyboard.KeyboardSwitcher;
27 import com.android.inputmethod.latin.EditingUtils;
28 import com.android.inputmethod.latin.LatinIME;
29 import com.android.inputmethod.latin.LatinIME.UIHandler;
30 import com.android.inputmethod.latin.LatinImeLogger;
31 import com.android.inputmethod.latin.R;
32 import com.android.inputmethod.latin.SubtypeSwitcher;
33 import com.android.inputmethod.latin.SuggestedWords;
34 import com.android.inputmethod.latin.Utils;
35 
36 import android.app.AlertDialog;
37 import android.content.ContentResolver;
38 import android.content.Context;
39 import android.content.DialogInterface;
40 import android.content.Intent;
41 import android.content.SharedPreferences;
42 import android.content.res.Configuration;
43 import android.net.Uri;
44 import android.os.AsyncTask;
45 import android.os.IBinder;
46 import android.preference.PreferenceManager;
47 import android.provider.Browser;
48 import android.speech.SpeechRecognizer;
49 import android.text.SpannableStringBuilder;
50 import android.text.Spanned;
51 import android.text.TextUtils;
52 import android.text.method.LinkMovementMethod;
53 import android.text.style.URLSpan;
54 import android.util.Log;
55 import android.view.LayoutInflater;
56 import android.view.View;
57 import android.view.ViewGroup;
58 import android.view.ViewParent;
59 import android.view.Window;
60 import android.view.WindowManager;
61 import android.view.inputmethod.EditorInfo;
62 import android.view.inputmethod.ExtractedTextRequest;
63 import android.view.inputmethod.InputConnection;
64 import android.widget.TextView;
65 
66 import java.util.ArrayList;
67 import java.util.HashMap;
68 import java.util.List;
69 import java.util.Map;
70 
71 public class VoiceProxy implements VoiceInput.UiListener {
72     private static final VoiceProxy sInstance = new VoiceProxy();
73 
74     public static final boolean VOICE_INSTALLED =
75             !InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED;
76     private static final boolean ENABLE_VOICE_BUTTON = true;
77     private static final String PREF_VOICE_MODE = "voice_mode";
78     // Whether or not the user has used voice input before (and thus, whether to show the
79     // first-run warning dialog or not).
80     private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input";
81     // Whether or not the user has used voice input from an unsupported locale UI before.
82     // For example, the user has a Chinese UI but activates voice input.
83     private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE =
84             "has_used_voice_input_unsupported_locale";
85     private static final int RECOGNITIONVIEW_HEIGHT_THRESHOLD_RATIO = 6;
86     // TODO: Adjusted on phones for now
87     private static final int RECOGNITIONVIEW_MINIMUM_HEIGHT_DIP = 244;
88 
89     private static final String TAG = VoiceProxy.class.getSimpleName();
90     private static final boolean DEBUG = LatinImeLogger.sDBG;
91 
92     private boolean mAfterVoiceInput;
93     private boolean mHasUsedVoiceInput;
94     private boolean mHasUsedVoiceInputUnsupportedLocale;
95     private boolean mImmediatelyAfterVoiceInput;
96     private boolean mIsShowingHint;
97     private boolean mLocaleSupportedForVoiceInput;
98     private boolean mPasswordText;
99     private boolean mRecognizing;
100     private boolean mShowingVoiceSuggestions;
101     private boolean mVoiceButtonEnabled;
102     private boolean mVoiceButtonOnPrimary;
103     private boolean mVoiceInputHighlighted;
104 
105     private int mMinimumVoiceRecognitionViewHeightPixel;
106     private InputMethodManagerCompatWrapper mImm;
107     private LatinIME mService;
108     private AlertDialog mVoiceWarningDialog;
109     private VoiceInput mVoiceInput;
110     private final VoiceResults mVoiceResults = new VoiceResults();
111     private Hints mHints;
112     private UIHandler mHandler;
113     private SubtypeSwitcher mSubtypeSwitcher;
114 
115     // For each word, a list of potential replacements, usually from voice.
116     private final Map<String, List<CharSequence>> mWordToSuggestions =
117             new HashMap<String, List<CharSequence>>();
118 
init(LatinIME context, SharedPreferences prefs, UIHandler h)119     public static VoiceProxy init(LatinIME context, SharedPreferences prefs, UIHandler h) {
120         sInstance.initInternal(context, prefs, h);
121         return sInstance;
122     }
123 
getInstance()124     public static VoiceProxy getInstance() {
125         return sInstance;
126     }
127 
initInternal(LatinIME service, SharedPreferences prefs, UIHandler h)128     private void initInternal(LatinIME service, SharedPreferences prefs, UIHandler h) {
129         if (!VOICE_INSTALLED) {
130             return;
131         }
132         mService = service;
133         mHandler = h;
134         mMinimumVoiceRecognitionViewHeightPixel = Utils.dipToPixel(
135                 Utils.getDipScale(service), RECOGNITIONVIEW_MINIMUM_HEIGHT_DIP);
136         mImm = InputMethodManagerCompatWrapper.getInstance();
137         mSubtypeSwitcher = SubtypeSwitcher.getInstance();
138         mVoiceInput = new VoiceInput(service, this);
139         mHints = new Hints(service, prefs, new Hints.Display() {
140             @Override
141             public void showHint(int viewResource) {
142                 View view = LayoutInflater.from(mService).inflate(viewResource, null);
143                 mIsShowingHint = true;
144             }
145         });
146     }
147 
VoiceProxy()148     private VoiceProxy() {
149         // Intentional empty constructor for singleton.
150     }
151 
resetVoiceStates(boolean isPasswordText)152     public void resetVoiceStates(boolean isPasswordText) {
153         mAfterVoiceInput = false;
154         mImmediatelyAfterVoiceInput = false;
155         mShowingVoiceSuggestions = false;
156         mVoiceInputHighlighted = false;
157         mPasswordText = isPasswordText;
158     }
159 
flushVoiceInputLogs(boolean configurationChanged)160     public void flushVoiceInputLogs(boolean configurationChanged) {
161         if (!VOICE_INSTALLED) {
162             return;
163         }
164         if (!configurationChanged) {
165             if (mAfterVoiceInput) {
166                 mVoiceInput.flushAllTextModificationCounters();
167                 mVoiceInput.logInputEnded();
168             }
169             mVoiceInput.flushLogs();
170             mVoiceInput.cancel();
171         }
172     }
173 
flushAndLogAllTextModificationCounters(int index, CharSequence suggestion, String wordSeparators)174     public void flushAndLogAllTextModificationCounters(int index, CharSequence suggestion,
175             String wordSeparators) {
176         if (!VOICE_INSTALLED) {
177             return;
178         }
179         if (mAfterVoiceInput && mShowingVoiceSuggestions) {
180             mVoiceInput.flushAllTextModificationCounters();
181             // send this intent AFTER logging any prior aggregated edits.
182             mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index,
183                     wordSeparators, mService.getCurrentInputConnection());
184         }
185     }
186 
showVoiceWarningDialog(final boolean swipe, IBinder token)187     private void showVoiceWarningDialog(final boolean swipe, IBinder token) {
188         if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
189             return;
190         }
191         AlertDialog.Builder builder = new UrlLinkAlertDialogBuilder(mService);
192         builder.setCancelable(true);
193         builder.setIcon(R.drawable.ic_mic_dialog);
194         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
195             @Override
196             public void onClick(DialogInterface dialog, int whichButton) {
197                 mVoiceInput.logKeyboardWarningDialogOk();
198                 reallyStartListening(swipe);
199             }
200         });
201         builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
202             @Override
203             public void onClick(DialogInterface dialog, int whichButton) {
204                 mVoiceInput.logKeyboardWarningDialogCancel();
205                 switchToLastInputMethod();
206             }
207         });
208         // When the dialog is dismissed by user's cancellation, switch back to the last input method
209         builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
210             @Override
211             public void onCancel(DialogInterface arg0) {
212                 mVoiceInput.logKeyboardWarningDialogCancel();
213                 switchToLastInputMethod();
214             }
215         });
216 
217         final CharSequence message;
218         if (mLocaleSupportedForVoiceInput) {
219             message = TextUtils.concat(
220                     mService.getText(R.string.voice_warning_may_not_understand), "\n\n",
221                             mService.getText(R.string.voice_warning_how_to_turn_off));
222         } else {
223             message = TextUtils.concat(
224                     mService.getText(R.string.voice_warning_locale_not_supported), "\n\n",
225                             mService.getText(R.string.voice_warning_may_not_understand), "\n\n",
226                                     mService.getText(R.string.voice_warning_how_to_turn_off));
227         }
228         builder.setMessage(message);
229         builder.setTitle(R.string.voice_warning_title);
230         mVoiceWarningDialog = builder.create();
231         final Window window = mVoiceWarningDialog.getWindow();
232         final WindowManager.LayoutParams lp = window.getAttributes();
233         lp.token = token;
234         lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
235         window.setAttributes(lp);
236         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
237         mVoiceInput.logKeyboardWarningDialogShown();
238         mVoiceWarningDialog.show();
239     }
240 
241     private static class UrlLinkAlertDialogBuilder extends AlertDialog.Builder {
242         private AlertDialog mAlertDialog;
243 
UrlLinkAlertDialogBuilder(Context context)244         public UrlLinkAlertDialogBuilder(Context context) {
245             super(context);
246         }
247 
248         @Override
setMessage(CharSequence message)249         public AlertDialog.Builder setMessage(CharSequence message) {
250             return super.setMessage(replaceURLSpan(message));
251         }
252 
replaceURLSpan(CharSequence message)253         private Spanned replaceURLSpan(CharSequence message) {
254             // Replace all spans with the custom span
255             final SpannableStringBuilder ssb = new SpannableStringBuilder(message);
256             for (URLSpan span : ssb.getSpans(0, ssb.length(), URLSpan.class)) {
257                 int spanStart = ssb.getSpanStart(span);
258                 int spanEnd = ssb.getSpanEnd(span);
259                 int spanFlags = ssb.getSpanFlags(span);
260                 ssb.removeSpan(span);
261                 ssb.setSpan(new ClickableSpan(span.getURL()), spanStart, spanEnd, spanFlags);
262             }
263             return ssb;
264         }
265 
266         @Override
create()267         public AlertDialog create() {
268             final AlertDialog dialog = super.create();
269 
270             dialog.setOnShowListener(new DialogInterface.OnShowListener() {
271                 @Override
272                 public void onShow(DialogInterface dialogInterface) {
273                     // Make URL in the dialog message click-able.
274                     TextView textView = (TextView) mAlertDialog.findViewById(android.R.id.message);
275                     if (textView != null) {
276                         textView.setMovementMethod(LinkMovementMethod.getInstance());
277                     }
278                 }
279             });
280             mAlertDialog = dialog;
281             return dialog;
282         }
283 
284         class ClickableSpan extends URLSpan {
ClickableSpan(String url)285             public ClickableSpan(String url) {
286                 super(url);
287             }
288 
289             @Override
onClick(View widget)290             public void onClick(View widget) {
291                 Uri uri = Uri.parse(getURL());
292                 Context context = widget.getContext();
293                 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
294                 // Add this flag to start an activity from service
295                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
296                 intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
297                 // Dismiss the warning dialog and go back to the previous IME.
298                 // TODO: If we can find a way to bring the new activity to front while keeping
299                 // the warning dialog, we don't need to dismiss it here.
300                 mAlertDialog.cancel();
301                 context.startActivity(intent);
302             }
303         }
304     }
305 
showPunctuationHintIfNecessary()306     public void showPunctuationHintIfNecessary() {
307         if (!VOICE_INSTALLED) {
308             return;
309         }
310         InputConnection ic = mService.getCurrentInputConnection();
311         if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) {
312             if (mHints.showPunctuationHintIfNecessary(ic)) {
313                 mVoiceInput.logPunctuationHintDisplayed();
314             }
315         }
316         mImmediatelyAfterVoiceInput = false;
317     }
318 
hideVoiceWindow(boolean configurationChanging)319     public void hideVoiceWindow(boolean configurationChanging) {
320         if (!VOICE_INSTALLED) {
321             return;
322         }
323         if (!configurationChanging) {
324             if (mAfterVoiceInput)
325                 mVoiceInput.logInputEnded();
326             if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
327                 mVoiceInput.logKeyboardWarningDialogDismissed();
328                 mVoiceWarningDialog.dismiss();
329                 mVoiceWarningDialog = null;
330             }
331             if (VOICE_INSTALLED & mRecognizing) {
332                 mVoiceInput.cancel();
333             }
334         }
335         mWordToSuggestions.clear();
336     }
337 
setCursorAndSelection(int newSelEnd, int newSelStart)338     public void setCursorAndSelection(int newSelEnd, int newSelStart) {
339         if (!VOICE_INSTALLED) {
340             return;
341         }
342         if (mAfterVoiceInput) {
343             mVoiceInput.setCursorPos(newSelEnd);
344             mVoiceInput.setSelectionSpan(newSelEnd - newSelStart);
345         }
346     }
347 
setVoiceInputHighlighted(boolean b)348     public void setVoiceInputHighlighted(boolean b) {
349         mVoiceInputHighlighted = b;
350     }
351 
setShowingVoiceSuggestions(boolean b)352     public void setShowingVoiceSuggestions(boolean b) {
353         mShowingVoiceSuggestions = b;
354     }
355 
isVoiceButtonEnabled()356     public boolean isVoiceButtonEnabled() {
357         return mVoiceButtonEnabled;
358     }
359 
isVoiceButtonOnPrimary()360     public boolean isVoiceButtonOnPrimary() {
361         return mVoiceButtonOnPrimary;
362     }
363 
isVoiceInputHighlighted()364     public boolean isVoiceInputHighlighted() {
365         return mVoiceInputHighlighted;
366     }
367 
isRecognizing()368     public boolean isRecognizing() {
369         return mRecognizing;
370     }
371 
needsToShowWarningDialog()372     public boolean needsToShowWarningDialog() {
373         return !mHasUsedVoiceInput
374                 || (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale);
375     }
376 
getAndResetIsShowingHint()377     public boolean getAndResetIsShowingHint() {
378         boolean ret = mIsShowingHint;
379         mIsShowingHint = false;
380         return ret;
381     }
382 
revertVoiceInput()383     private void revertVoiceInput() {
384         InputConnection ic = mService.getCurrentInputConnection();
385         if (ic != null) ic.commitText("", 1);
386         mService.updateSuggestions();
387         mVoiceInputHighlighted = false;
388     }
389 
commitVoiceInput()390     public void commitVoiceInput() {
391         if (VOICE_INSTALLED && mVoiceInputHighlighted) {
392             InputConnection ic = mService.getCurrentInputConnection();
393             if (ic != null) ic.finishComposingText();
394             mService.updateSuggestions();
395             mVoiceInputHighlighted = false;
396         }
397     }
398 
logAndRevertVoiceInput()399     public boolean logAndRevertVoiceInput() {
400         if (!VOICE_INSTALLED) {
401             return false;
402         }
403         if (mVoiceInputHighlighted) {
404             mVoiceInput.incrementTextModificationDeleteCount(
405                     mVoiceResults.candidates.get(0).toString().length());
406             revertVoiceInput();
407             return true;
408         } else {
409             return false;
410         }
411     }
412 
rememberReplacedWord(CharSequence suggestion,String wordSeparators)413     public void rememberReplacedWord(CharSequence suggestion,String wordSeparators) {
414         if (!VOICE_INSTALLED) {
415             return;
416         }
417         if (mShowingVoiceSuggestions) {
418             // Retain the replaced word in the alternatives array.
419             String wordToBeReplaced = EditingUtils.getWordAtCursor(
420                     mService.getCurrentInputConnection(), wordSeparators);
421             if (!mWordToSuggestions.containsKey(wordToBeReplaced)) {
422                 wordToBeReplaced = wordToBeReplaced.toLowerCase();
423             }
424             if (mWordToSuggestions.containsKey(wordToBeReplaced)) {
425                 List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced);
426                 if (suggestions.contains(suggestion)) {
427                     suggestions.remove(suggestion);
428                 }
429                 suggestions.add(wordToBeReplaced);
430                 mWordToSuggestions.remove(wordToBeReplaced);
431                 mWordToSuggestions.put(suggestion.toString(), suggestions);
432             }
433         }
434     }
435 
436     /**
437      * Tries to apply any voice alternatives for the word if this was a spoken word and
438      * there are voice alternatives.
439      * @param touching The word that the cursor is touching, with position information
440      * @return true if an alternative was found, false otherwise.
441      */
applyVoiceAlternatives(EditingUtils.SelectedWord touching)442     public boolean applyVoiceAlternatives(EditingUtils.SelectedWord touching) {
443         if (!VOICE_INSTALLED) {
444             return false;
445         }
446         // Search for result in spoken word alternatives
447         String selectedWord = touching.mWord.toString().trim();
448         if (!mWordToSuggestions.containsKey(selectedWord)) {
449             selectedWord = selectedWord.toLowerCase();
450         }
451         if (mWordToSuggestions.containsKey(selectedWord)) {
452             mShowingVoiceSuggestions = true;
453             List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
454             SuggestedWords.Builder builder = new SuggestedWords.Builder();
455             // If the first letter of touching is capitalized, make all the suggestions
456             // start with a capital letter.
457             if (Character.isUpperCase(touching.mWord.charAt(0))) {
458                 for (CharSequence word : suggestions) {
459                     String str = word.toString();
460                     word = Character.toUpperCase(str.charAt(0)) + str.substring(1);
461                     builder.addWord(word);
462                 }
463             } else {
464                 builder.addWords(suggestions, null);
465             }
466             builder.setTypedWordValid(true).setHasMinimalSuggestion(true);
467             mService.setSuggestions(builder.build());
468 //            mService.setCandidatesViewShown(true);
469             return true;
470         }
471         return false;
472     }
473 
handleBackspace()474     public void handleBackspace() {
475         if (!VOICE_INSTALLED) {
476             return;
477         }
478         if (mAfterVoiceInput) {
479             // Don't log delete if the user is pressing delete at
480             // the beginning of the text box (hence not deleting anything)
481             if (mVoiceInput.getCursorPos() > 0) {
482                 // If anything was selected before the delete was pressed, increment the
483                 // delete count by the length of the selection
484                 int deleteLen  =  mVoiceInput.getSelectionSpan() > 0 ?
485                         mVoiceInput.getSelectionSpan() : 1;
486                 mVoiceInput.incrementTextModificationDeleteCount(deleteLen);
487             }
488         }
489     }
490 
handleCharacter()491     public void handleCharacter() {
492         if (!VOICE_INSTALLED) {
493             return;
494         }
495         commitVoiceInput();
496         if (mAfterVoiceInput) {
497             // Assume input length is 1. This assumption fails for smiley face insertions.
498             mVoiceInput.incrementTextModificationInsertCount(1);
499         }
500     }
501 
handleSeparator()502     public void handleSeparator() {
503         if (!VOICE_INSTALLED) {
504             return;
505         }
506         commitVoiceInput();
507         if (mAfterVoiceInput){
508             // Assume input length is 1. This assumption fails for smiley face insertions.
509             mVoiceInput.incrementTextModificationInsertPunctuationCount(1);
510         }
511     }
512 
handleClose()513     public void handleClose() {
514         if (!VOICE_INSTALLED) {
515             return;
516         }
517         if (mRecognizing) {
518             mVoiceInput.cancel();
519         }
520     }
521 
522 
handleVoiceResults(boolean capitalizeFirstWord)523     public void handleVoiceResults(boolean capitalizeFirstWord) {
524         if (!VOICE_INSTALLED) {
525             return;
526         }
527         mAfterVoiceInput = true;
528         mImmediatelyAfterVoiceInput = true;
529 
530         InputConnection ic = mService.getCurrentInputConnection();
531         if (!mService.isFullscreenMode()) {
532             // Start listening for updates to the text from typing, etc.
533             if (ic != null) {
534                 ExtractedTextRequest req = new ExtractedTextRequest();
535                 ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR);
536             }
537         }
538         mService.vibrate();
539 
540         final List<CharSequence> nBest = new ArrayList<CharSequence>();
541         for (String c : mVoiceResults.candidates) {
542             if (capitalizeFirstWord) {
543                 c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length());
544             }
545             nBest.add(c);
546         }
547         if (nBest.size() == 0) {
548             return;
549         }
550         String bestResult = nBest.get(0).toString();
551         mVoiceInput.logVoiceInputDelivered(bestResult.length());
552         mHints.registerVoiceResult(bestResult);
553 
554         if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text
555         mService.commitTyped(ic);
556         EditingUtils.appendText(ic, bestResult);
557         if (ic != null) ic.endBatchEdit();
558 
559         mVoiceInputHighlighted = true;
560         mWordToSuggestions.putAll(mVoiceResults.alternatives);
561         onCancelVoice();
562     }
563 
switchToRecognitionStatusView(final Configuration configuration)564     public void switchToRecognitionStatusView(final Configuration configuration) {
565         if (!VOICE_INSTALLED) {
566             return;
567         }
568         mHandler.post(new Runnable() {
569             @Override
570             public void run() {
571 //                mService.setCandidatesViewShown(false);
572                 mRecognizing = true;
573                 mVoiceInput.newView();
574                 View v = mVoiceInput.getView();
575 
576                 ViewParent p = v.getParent();
577                 if (p != null && p instanceof ViewGroup) {
578                     ((ViewGroup) p).removeView(v);
579                 }
580 
581                 View keyboardView = KeyboardSwitcher.getInstance().getKeyboardView();
582 
583                 // The full height of the keyboard is difficult to calculate
584                 // as the dimension is expressed in "mm" and not in "pixel"
585                 // As we add mm, we don't know how the rounding is going to work
586                 // thus we may end up with few pixels extra (or less).
587                 if (keyboardView != null) {
588                     View popupLayout = v.findViewById(R.id.popup_layout);
589                     final int displayHeight =
590                             mService.getResources().getDisplayMetrics().heightPixels;
591                     final int currentHeight = popupLayout.getLayoutParams().height;
592                     final int keyboardHeight = keyboardView.getHeight();
593                     if (mMinimumVoiceRecognitionViewHeightPixel > keyboardHeight
594                             || mMinimumVoiceRecognitionViewHeightPixel > currentHeight) {
595                         popupLayout.getLayoutParams().height =
596                             mMinimumVoiceRecognitionViewHeightPixel;
597                     } else if (keyboardHeight > currentHeight || keyboardHeight
598                             > (displayHeight / RECOGNITIONVIEW_HEIGHT_THRESHOLD_RATIO)) {
599                         popupLayout.getLayoutParams().height = keyboardHeight;
600                     }
601                 }
602                 mService.setInputView(v);
603                 mService.updateInputViewShown();
604 
605                 if (configuration != null) {
606                     mVoiceInput.onConfigurationChanged(configuration);
607                 }
608         }});
609     }
610 
switchToLastInputMethod()611     private void switchToLastInputMethod() {
612         if (!VOICE_INSTALLED) {
613             return;
614         }
615         final IBinder token = mService.getWindow().getWindow().getAttributes().token;
616         new AsyncTask<Void, Void, Boolean>() {
617             @Override
618             protected Boolean doInBackground(Void... params) {
619                 return mImm.switchToLastInputMethod(token);
620             }
621 
622             @Override
623             protected void onPostExecute(Boolean result) {
624                 // Calls in this method need to be done in the same thread as the thread which
625                 // called switchToLastInputMethod()
626                 if (!result) {
627                     if (DEBUG) {
628                         Log.d(TAG, "Couldn't switch back to last IME.");
629                     }
630                     // Because the current IME and subtype failed to switch to any other IME and
631                     // subtype by switchToLastInputMethod, the current IME and subtype should keep
632                     // being LatinIME and voice subtype in the next time. And for re-showing voice
633                     // mode, the state of voice input should be reset and the voice view should be
634                     // hidden.
635                     mVoiceInput.reset();
636                     mService.requestHideSelf(0);
637                 } else {
638                     // Notify an event that the current subtype was changed. This event will be
639                     // handled if "onCurrentInputMethodSubtypeChanged" can't be implemented
640                     // when the API level is 10 or previous.
641                     mService.notifyOnCurrentInputMethodSubtypeChanged(null);
642                 }
643             }
644         }.execute();
645     }
646 
reallyStartListening(boolean swipe)647     private void reallyStartListening(boolean swipe) {
648         if (!VOICE_INSTALLED) {
649             return;
650         }
651         if (!mHasUsedVoiceInput) {
652             // The user has started a voice input, so remember that in the
653             // future (so we don't show the warning dialog after the first run).
654             SharedPreferences.Editor editor =
655                     PreferenceManager.getDefaultSharedPreferences(mService).edit();
656             editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true);
657             SharedPreferencesCompat.apply(editor);
658             mHasUsedVoiceInput = true;
659         }
660 
661         if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) {
662             // The user has started a voice input from an unsupported locale, so remember that
663             // in the future (so we don't show the warning dialog the next time they do this).
664             SharedPreferences.Editor editor =
665                     PreferenceManager.getDefaultSharedPreferences(mService).edit();
666             editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true);
667             SharedPreferencesCompat.apply(editor);
668             mHasUsedVoiceInputUnsupportedLocale = true;
669         }
670 
671         // Clear N-best suggestions
672         mService.clearSuggestions();
673 
674         FieldContext context = makeFieldContext();
675         mVoiceInput.startListening(context, swipe);
676         switchToRecognitionStatusView(null);
677     }
678 
startListening(final boolean swipe, IBinder token)679     public void startListening(final boolean swipe, IBinder token) {
680         if (!VOICE_INSTALLED) {
681             return;
682         }
683         // TODO: remove swipe which is no longer used.
684         if (needsToShowWarningDialog()) {
685             // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel.
686             showVoiceWarningDialog(swipe, token);
687         } else {
688             reallyStartListening(swipe);
689         }
690     }
691 
fieldCanDoVoice(FieldContext fieldContext)692     private boolean fieldCanDoVoice(FieldContext fieldContext) {
693         return !mPasswordText
694                 && mVoiceInput != null
695                 && !mVoiceInput.isBlacklistedField(fieldContext);
696     }
697 
shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute)698     private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) {
699         @SuppressWarnings("deprecation")
700         final boolean noMic = Utils.inPrivateImeOptions(null,
701                 LatinIME.IME_OPTION_NO_MICROPHONE_COMPAT, attribute)
702                 || Utils.inPrivateImeOptions(mService.getPackageName(),
703                         LatinIME.IME_OPTION_NO_MICROPHONE, attribute);
704         return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext) && !noMic
705                 && SpeechRecognizer.isRecognitionAvailable(mService);
706     }
707 
isRecognitionAvailable(Context context)708     public static boolean isRecognitionAvailable(Context context) {
709         return SpeechRecognizer.isRecognitionAvailable(context);
710     }
711 
loadSettings(EditorInfo attribute, SharedPreferences sp)712     public void loadSettings(EditorInfo attribute, SharedPreferences sp) {
713         if (!VOICE_INSTALLED) {
714             return;
715         }
716         mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false);
717         mHasUsedVoiceInputUnsupportedLocale =
718                 sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false);
719 
720         mLocaleSupportedForVoiceInput = SubtypeSwitcher.isVoiceSupported(
721                 mService, SubtypeSwitcher.getInstance().getInputLocaleStr());
722 
723         final String voiceMode = sp.getString(PREF_VOICE_MODE,
724                 mService.getString(R.string.voice_mode_main));
725         mVoiceButtonEnabled = !voiceMode.equals(mService.getString(R.string.voice_mode_off))
726                 && shouldShowVoiceButton(makeFieldContext(), attribute);
727         mVoiceButtonOnPrimary = voiceMode.equals(mService.getString(R.string.voice_mode_main));
728     }
729 
destroy()730     public void destroy() {
731         if (!VOICE_INSTALLED) {
732             return;
733         }
734         if (mVoiceInput != null) {
735             mVoiceInput.destroy();
736         }
737     }
738 
onStartInputView(IBinder keyboardViewToken)739     public void onStartInputView(IBinder keyboardViewToken) {
740         if (!VOICE_INSTALLED) {
741             return;
742         }
743         // If keyboardViewToken is null, keyboardView is not attached but voiceView is attached.
744         IBinder windowToken = keyboardViewToken != null ? keyboardViewToken
745                 : mVoiceInput.getView().getWindowToken();
746         // If IME is in voice mode, but still needs to show the voice warning dialog,
747         // keep showing the warning.
748         if (mSubtypeSwitcher.isVoiceMode() && windowToken != null) {
749             // Close keyboard view if it is been shown.
750             if (KeyboardSwitcher.getInstance().isInputViewShown())
751                 KeyboardSwitcher.getInstance().getKeyboardView().purgeKeyboardAndClosing();
752             startListening(false, windowToken);
753         }
754         // If we have no token, onAttachedToWindow will take care of showing dialog and start
755         // listening.
756     }
757 
onAttachedToWindow()758     public void onAttachedToWindow() {
759         if (!VOICE_INSTALLED) {
760             return;
761         }
762         // After onAttachedToWindow, we can show the voice warning dialog. See startListening()
763         // above.
764         VoiceInputWrapper.getInstance().setVoiceInput(mVoiceInput, mSubtypeSwitcher);
765     }
766 
onConfigurationChanged(Configuration configuration)767     public void onConfigurationChanged(Configuration configuration) {
768         if (!VOICE_INSTALLED) {
769             return;
770         }
771         if (mRecognizing) {
772             switchToRecognitionStatusView(configuration);
773         }
774     }
775 
776     @Override
onCancelVoice()777     public void onCancelVoice() {
778         if (!VOICE_INSTALLED) {
779             return;
780         }
781         if (mRecognizing) {
782             if (mSubtypeSwitcher.isVoiceMode()) {
783                 // If voice mode is being canceled within LatinIME (i.e. time-out or user
784                 // cancellation etc.), onCancelVoice() will be called first. LatinIME thinks it's
785                 // still in voice mode. LatinIME needs to call switchToLastInputMethod().
786                 // Note that onCancelVoice() will be called again from SubtypeSwitcher.
787                 switchToLastInputMethod();
788             } else if (mSubtypeSwitcher.isKeyboardMode()) {
789                 // If voice mode is being canceled out of LatinIME (i.e. by user's IME switching or
790                 // as a result of switchToLastInputMethod() etc.),
791                 // onCurrentInputMethodSubtypeChanged() will be called first. LatinIME will know
792                 // that it's in keyboard mode and SubtypeSwitcher will call onCancelVoice().
793                 mRecognizing = false;
794                 mService.switchToKeyboardView();
795             }
796         }
797     }
798 
799     @Override
onVoiceResults(List<String> candidates, Map<String, List<CharSequence>> alternatives)800     public void onVoiceResults(List<String> candidates,
801             Map<String, List<CharSequence>> alternatives) {
802         if (!VOICE_INSTALLED) {
803             return;
804         }
805         if (!mRecognizing) {
806             return;
807         }
808         mVoiceResults.candidates = candidates;
809         mVoiceResults.alternatives = alternatives;
810         mHandler.updateVoiceResults();
811     }
812 
makeFieldContext()813     private FieldContext makeFieldContext() {
814         SubtypeSwitcher switcher = SubtypeSwitcher.getInstance();
815         return new FieldContext(mService.getCurrentInputConnection(),
816                 mService.getCurrentInputEditorInfo(), switcher.getInputLocaleStr(),
817                 switcher.getEnabledLanguages());
818     }
819 
820     // TODO: make this private (proguard issue)
821     public static class VoiceResults {
822         List<String> candidates;
823         Map<String, List<CharSequence>> alternatives;
824     }
825 
826     public static class VoiceInputWrapper {
827         private static final VoiceInputWrapper sInputWrapperInstance = new VoiceInputWrapper();
828         private VoiceInput mVoiceInput;
getInstance()829         public static VoiceInputWrapper getInstance() {
830             return sInputWrapperInstance;
831         }
setVoiceInput(VoiceInput voiceInput, SubtypeSwitcher switcher)832         private void setVoiceInput(VoiceInput voiceInput, SubtypeSwitcher switcher) {
833             if (!VOICE_INSTALLED) {
834                 return;
835             }
836             if (mVoiceInput == null && voiceInput != null) {
837                 mVoiceInput = voiceInput;
838             }
839             switcher.setVoiceInputWrapper(this);
840         }
841 
VoiceInputWrapper()842         private VoiceInputWrapper() {
843         }
844 
cancel()845         public void cancel() {
846             if (!VOICE_INSTALLED) {
847                 return;
848             }
849             if (mVoiceInput != null) mVoiceInput.cancel();
850         }
851 
reset()852         public void reset() {
853             if (!VOICE_INSTALLED) {
854                 return;
855             }
856             if (mVoiceInput != null) mVoiceInput.reset();
857         }
858     }
859 
860     // A list of locales which are supported by default for voice input, unless we get a
861     // different list from Gservices.
862     private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES =
863             "en " +
864             "en_US " +
865             "en_GB " +
866             "en_AU " +
867             "en_CA " +
868             "en_IE " +
869             "en_IN " +
870             "en_NZ " +
871             "en_SG " +
872             "en_ZA ";
873 
getSupportedLocalesString(ContentResolver resolver)874     public static String getSupportedLocalesString (ContentResolver resolver) {
875         return SettingsUtil.getSettingsString(
876                 resolver,
877                 SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES,
878                 DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES);
879     }
880 }
881