Home | History | Annotate | Download | only in deprecated
      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 
    119     public static VoiceProxy init(LatinIME context, SharedPreferences prefs, UIHandler h) {
    120         sInstance.initInternal(context, prefs, h);
    121         return sInstance;
    122     }
    123 
    124     public static VoiceProxy getInstance() {
    125         return sInstance;
    126     }
    127 
    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 
    148     private VoiceProxy() {
    149         // Intentional empty constructor for singleton.
    150     }
    151 
    152     public void resetVoiceStates(boolean isPasswordText) {
    153         mAfterVoiceInput = false;
    154         mImmediatelyAfterVoiceInput = false;
    155         mShowingVoiceSuggestions = false;
    156         mVoiceInputHighlighted = false;
    157         mPasswordText = isPasswordText;
    158     }
    159 
    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 
    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 
    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 
    244         public UrlLinkAlertDialogBuilder(Context context) {
    245             super(context);
    246         }
    247 
    248         @Override
    249         public AlertDialog.Builder setMessage(CharSequence message) {
    250             return super.setMessage(replaceURLSpan(message));
    251         }
    252 
    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
    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 {
    285             public ClickableSpan(String url) {
    286                 super(url);
    287             }
    288 
    289             @Override
    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 
    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 
    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 
    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 
    348     public void setVoiceInputHighlighted(boolean b) {
    349         mVoiceInputHighlighted = b;
    350     }
    351 
    352     public void setShowingVoiceSuggestions(boolean b) {
    353         mShowingVoiceSuggestions = b;
    354     }
    355 
    356     public boolean isVoiceButtonEnabled() {
    357         return mVoiceButtonEnabled;
    358     }
    359 
    360     public boolean isVoiceButtonOnPrimary() {
    361         return mVoiceButtonOnPrimary;
    362     }
    363 
    364     public boolean isVoiceInputHighlighted() {
    365         return mVoiceInputHighlighted;
    366     }
    367 
    368     public boolean isRecognizing() {
    369         return mRecognizing;
    370     }
    371 
    372     public boolean needsToShowWarningDialog() {
    373         return !mHasUsedVoiceInput
    374                 || (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale);
    375     }
    376 
    377     public boolean getAndResetIsShowingHint() {
    378         boolean ret = mIsShowingHint;
    379         mIsShowingHint = false;
    380         return ret;
    381     }
    382 
    383     private void revertVoiceInput() {
    384         InputConnection ic = mService.getCurrentInputConnection();
    385         if (ic != null) ic.commitText("", 1);
    386         mService.updateSuggestions();
    387         mVoiceInputHighlighted = false;
    388     }
    389 
    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 
    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 
    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      */
    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 
    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 
    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 
    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 
    513     public void handleClose() {
    514         if (!VOICE_INSTALLED) {
    515             return;
    516         }
    517         if (mRecognizing) {
    518             mVoiceInput.cancel();
    519         }
    520     }
    521 
    522 
    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 
    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 
    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 
    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 
    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 
    692     private boolean fieldCanDoVoice(FieldContext fieldContext) {
    693         return !mPasswordText
    694                 && mVoiceInput != null
    695                 && !mVoiceInput.isBlacklistedField(fieldContext);
    696     }
    697 
    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 
    708     public static boolean isRecognitionAvailable(Context context) {
    709         return SpeechRecognizer.isRecognitionAvailable(context);
    710     }
    711 
    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 
    730     public void destroy() {
    731         if (!VOICE_INSTALLED) {
    732             return;
    733         }
    734         if (mVoiceInput != null) {
    735             mVoiceInput.destroy();
    736         }
    737     }
    738 
    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 
    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 
    767     public void onConfigurationChanged(Configuration configuration) {
    768         if (!VOICE_INSTALLED) {
    769             return;
    770         }
    771         if (mRecognizing) {
    772             switchToRecognitionStatusView(configuration);
    773         }
    774     }
    775 
    776     @Override
    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
    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 
    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;
    829         public static VoiceInputWrapper getInstance() {
    830             return sInputWrapperInstance;
    831         }
    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 
    842         private VoiceInputWrapper() {
    843         }
    844 
    845         public void cancel() {
    846             if (!VOICE_INSTALLED) {
    847                 return;
    848             }
    849             if (mVoiceInput != null) mVoiceInput.cancel();
    850         }
    851 
    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 
    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