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