/ime/latinime/src/com/googlecode/eyesfree/inputmethod/voice/VoiceInput.java

http://eyes-free.googlecode.com/ · Java · 644 lines · 431 code · 87 blank · 126 comment · 37 complexity · d5632ef1607e76df7b58609f0d1bda04 MD5 · raw file

  1. /*
  2. * Copyright (C) 2009 Google Inc.
  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. package com.googlecode.eyesfree.inputmethod.voice;
  17. import android.content.ContentResolver;
  18. import android.content.Context;
  19. import android.content.Intent;
  20. import android.os.Build;
  21. import android.os.Bundle;
  22. import android.os.Handler;
  23. import android.os.Message;
  24. import android.os.Parcelable;
  25. import android.speech.RecognitionListener;
  26. import android.speech.RecognizerIntent;
  27. import android.speech.SpeechRecognizer;
  28. import android.util.Log;
  29. import android.view.View;
  30. import android.view.View.OnClickListener;
  31. import android.view.inputmethod.InputConnection;
  32. import com.googlecode.eyesfree.inputmethod.latin.EditingUtil;
  33. import com.googlecode.eyesfree.inputmethod.latin.R;
  34. import java.io.ByteArrayOutputStream;
  35. import java.io.IOException;
  36. import java.util.ArrayList;
  37. import java.util.HashMap;
  38. import java.util.List;
  39. import java.util.Locale;
  40. import java.util.Map;
  41. /**
  42. * Speech recognition input, including both user interface and a background
  43. * process to stream audio to the network recognizer. This class supplies a
  44. * View (getView()), which it updates as recognition occurs. The user of this
  45. * class is responsible for making the view visible to the user, as well as
  46. * handling various events returned through UiListener.
  47. */
  48. public class VoiceInput implements OnClickListener {
  49. private static final String TAG = "VoiceInput";
  50. private static final String EXTRA_RECOGNITION_CONTEXT =
  51. "android.speech.extras.RECOGNITION_CONTEXT";
  52. private static final String EXTRA_CALLING_PACKAGE = "calling_package";
  53. private static final String EXTRA_ALTERNATES = "android.speech.extra.ALTERNATES";
  54. private static final int MAX_ALT_LIST_LENGTH = 6;
  55. private static final String DEFAULT_RECOMMENDED_PACKAGES =
  56. "com.android.mms " +
  57. "com.google.android.gm " +
  58. "com.google.android.talk " +
  59. "com.google.android.apps.googlevoice " +
  60. "com.android.email " +
  61. "com.android.browser ";
  62. // WARNING! Before enabling this, fix the problem with calling getExtractedText() in
  63. // landscape view. It causes Extracted text updates to be rejected due to a token mismatch
  64. public static boolean ENABLE_WORD_CORRECTIONS = true;
  65. // Dummy word suggestion which means "delete current word"
  66. public static final String DELETE_SYMBOL = " \u00D7 "; // times symbol
  67. private Whitelist mRecommendedList;
  68. private Whitelist mBlacklist;
  69. private VoiceInputLogger mLogger;
  70. // Names of a few extras defined in VoiceSearch's RecognitionController
  71. // Note, the version of voicesearch that shipped in Froyo returns the raw
  72. // RecognitionClientAlternates protocol buffer under the key "alternates",
  73. // so a VS market update must be installed on Froyo devices in order to see
  74. // alternatives.
  75. private static final String ALTERNATES_BUNDLE = "alternates_bundle";
  76. // This is copied from the VoiceSearch app.
  77. private static final class AlternatesBundleKeys {
  78. public static final String ALTERNATES = "alternates";
  79. public static final String LENGTH = "length";
  80. public static final String SPANS = "spans";
  81. public static final String START = "start";
  82. public static final String TEXT = "text";
  83. }
  84. // Names of a few intent extras defined in VoiceSearch's RecognitionService.
  85. // These let us tweak the endpointer parameters.
  86. private static final String EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS =
  87. "android.speech.extras.SPEECH_INPUT_MINIMUM_LENGTH_MILLIS";
  88. private static final String EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS =
  89. "android.speech.extras.SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS";
  90. private static final String EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS =
  91. "android.speech.extras.SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS";
  92. // The usual endpointer default value for input complete silence length is 0.5 seconds,
  93. // but that's used for things like voice search. For dictation-like voice input like this,
  94. // we go with a more liberal value of 1 second. This value will only be used if a value
  95. // is not provided from Gservices.
  96. private static final String INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS = "1000";
  97. // Used to record part of that state for logging purposes.
  98. public static final int DEFAULT = 0;
  99. public static final int LISTENING = 1;
  100. public static final int WORKING = 2;
  101. public static final int ERROR = 3;
  102. private int mAfterVoiceInputDeleteCount = 0;
  103. private int mAfterVoiceInputInsertCount = 0;
  104. private int mAfterVoiceInputInsertPunctuationCount = 0;
  105. private int mAfterVoiceInputCursorPos = 0;
  106. private int mAfterVoiceInputSelectionSpan = 0;
  107. private int mState = DEFAULT;
  108. private final static int MSG_CLOSE_ERROR_DIALOG = 1;
  109. private final Handler mHandler = new Handler() {
  110. @Override
  111. public void handleMessage(Message msg) {
  112. if (msg.what == MSG_CLOSE_ERROR_DIALOG) {
  113. mState = DEFAULT;
  114. mRecognitionView.finish();
  115. mUiListener.onCancelVoice();
  116. }
  117. }
  118. };
  119. /**
  120. * Events relating to the recognition UI. You must implement these.
  121. */
  122. public interface UiListener {
  123. /**
  124. * @param recognitionResults a set of transcripts for what the user
  125. * spoke, sorted by likelihood.
  126. */
  127. public void onVoiceResults(
  128. List<String> recognitionResults,
  129. Map<String, List<CharSequence>> alternatives);
  130. /**
  131. * Called when the user cancels speech recognition.
  132. */
  133. public void onCancelVoice();
  134. }
  135. private SpeechRecognizer mSpeechRecognizer;
  136. private RecognitionListener mRecognitionListener;
  137. private RecognitionView mRecognitionView;
  138. private UiListener mUiListener;
  139. private Context mContext;
  140. /**
  141. * @param context the service or activity in which we're running.
  142. * @param uiHandler object to receive events from VoiceInput.
  143. */
  144. public VoiceInput(Context context, UiListener uiHandler) {
  145. mLogger = VoiceInputLogger.getLogger(context);
  146. mRecognitionListener = new ImeRecognitionListener();
  147. mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
  148. mSpeechRecognizer.setRecognitionListener(mRecognitionListener);
  149. mUiListener = uiHandler;
  150. mContext = context;
  151. newView();
  152. String recommendedPackages = SettingsUtil.getSettingsString(
  153. context.getContentResolver(),
  154. SettingsUtil.LATIN_IME_VOICE_INPUT_RECOMMENDED_PACKAGES,
  155. DEFAULT_RECOMMENDED_PACKAGES);
  156. mRecommendedList = new Whitelist();
  157. for (String recommendedPackage : recommendedPackages.split("\\s+")) {
  158. mRecommendedList.addApp(recommendedPackage);
  159. }
  160. mBlacklist = new Whitelist();
  161. mBlacklist.addApp("com.android.setupwizard");
  162. }
  163. public void setCursorPos(int pos) {
  164. mAfterVoiceInputCursorPos = pos;
  165. }
  166. public int getCursorPos() {
  167. return mAfterVoiceInputCursorPos;
  168. }
  169. public void setSelectionSpan(int span) {
  170. mAfterVoiceInputSelectionSpan = span;
  171. }
  172. public int getSelectionSpan() {
  173. return mAfterVoiceInputSelectionSpan;
  174. }
  175. public void incrementTextModificationDeleteCount(int count){
  176. mAfterVoiceInputDeleteCount += count;
  177. // Send up intents for other text modification types
  178. if (mAfterVoiceInputInsertCount > 0) {
  179. logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
  180. mAfterVoiceInputInsertCount = 0;
  181. }
  182. if (mAfterVoiceInputInsertPunctuationCount > 0) {
  183. logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
  184. mAfterVoiceInputInsertPunctuationCount = 0;
  185. }
  186. }
  187. public void incrementTextModificationInsertCount(int count){
  188. mAfterVoiceInputInsertCount += count;
  189. if (mAfterVoiceInputSelectionSpan > 0) {
  190. // If text was highlighted before inserting the char, count this as
  191. // a delete.
  192. mAfterVoiceInputDeleteCount += mAfterVoiceInputSelectionSpan;
  193. }
  194. // Send up intents for other text modification types
  195. if (mAfterVoiceInputDeleteCount > 0) {
  196. logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
  197. mAfterVoiceInputDeleteCount = 0;
  198. }
  199. if (mAfterVoiceInputInsertPunctuationCount > 0) {
  200. logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
  201. mAfterVoiceInputInsertPunctuationCount = 0;
  202. }
  203. }
  204. public void incrementTextModificationInsertPunctuationCount(int count){
  205. mAfterVoiceInputInsertPunctuationCount += 1;
  206. if (mAfterVoiceInputSelectionSpan > 0) {
  207. // If text was highlighted before inserting the char, count this as
  208. // a delete.
  209. mAfterVoiceInputDeleteCount += mAfterVoiceInputSelectionSpan;
  210. }
  211. // Send up intents for aggregated non-punctuation insertions
  212. if (mAfterVoiceInputDeleteCount > 0) {
  213. logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
  214. mAfterVoiceInputDeleteCount = 0;
  215. }
  216. if (mAfterVoiceInputInsertCount > 0) {
  217. logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
  218. mAfterVoiceInputInsertCount = 0;
  219. }
  220. }
  221. public void flushAllTextModificationCounters() {
  222. if (mAfterVoiceInputInsertCount > 0) {
  223. logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
  224. mAfterVoiceInputInsertCount = 0;
  225. }
  226. if (mAfterVoiceInputDeleteCount > 0) {
  227. logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
  228. mAfterVoiceInputDeleteCount = 0;
  229. }
  230. if (mAfterVoiceInputInsertPunctuationCount > 0) {
  231. logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
  232. mAfterVoiceInputInsertPunctuationCount = 0;
  233. }
  234. }
  235. /**
  236. * The configuration of the IME changed and may have caused the views to be layed out
  237. * again. Restore the state of the recognition view.
  238. */
  239. public void onConfigurationChanged() {
  240. mRecognitionView.restoreState();
  241. }
  242. /**
  243. * @return true if field is blacklisted for voice
  244. */
  245. public boolean isBlacklistedField(FieldContext context) {
  246. return mBlacklist.matches(context);
  247. }
  248. /**
  249. * Used to decide whether to show voice input hints for this field, etc.
  250. *
  251. * @return true if field is recommended for voice
  252. */
  253. public boolean isRecommendedField(FieldContext context) {
  254. return mRecommendedList.matches(context);
  255. }
  256. /**
  257. * Start listening for speech from the user. This will grab the microphone
  258. * and start updating the view provided by getView(). It is the caller's
  259. * responsibility to ensure that the view is visible to the user at this stage.
  260. *
  261. * @param context the same FieldContext supplied to voiceIsEnabled()
  262. * @param swipe whether this voice input was started by swipe, for logging purposes
  263. */
  264. public void startListening(FieldContext context, boolean swipe) {
  265. mState = DEFAULT;
  266. Locale locale = Locale.getDefault();
  267. String localeString = locale.getLanguage() + "-" + locale.getCountry();
  268. mLogger.start(localeString, swipe);
  269. mState = LISTENING;
  270. mRecognitionView.showInitializing();
  271. startListeningAfterInitialization(context);
  272. }
  273. /**
  274. * Called only when the recognition manager's initialization completed
  275. *
  276. * @param context context with which {@link #startListening(FieldContext, boolean)} was executed
  277. */
  278. private void startListeningAfterInitialization(FieldContext context) {
  279. Intent intent = makeIntent();
  280. intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, "");
  281. intent.putExtra(EXTRA_RECOGNITION_CONTEXT, context.getBundle());
  282. intent.putExtra(EXTRA_CALLING_PACKAGE, "VoiceIME");
  283. intent.putExtra(EXTRA_ALTERNATES, true);
  284. intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,
  285. SettingsUtil.getSettingsInt(
  286. mContext.getContentResolver(),
  287. SettingsUtil.LATIN_IME_MAX_VOICE_RESULTS,
  288. 1));
  289. // Get endpointer params from Gservices.
  290. // TODO: Consider caching these values for improved performance on slower devices.
  291. final ContentResolver cr = mContext.getContentResolver();
  292. putEndpointerExtra(
  293. cr,
  294. intent,
  295. SettingsUtil.LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS,
  296. EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS,
  297. null /* rely on endpointer default */);
  298. putEndpointerExtra(
  299. cr,
  300. intent,
  301. SettingsUtil.LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
  302. EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
  303. INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS
  304. /* our default value is different from the endpointer's */);
  305. putEndpointerExtra(
  306. cr,
  307. intent,
  308. SettingsUtil.
  309. LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
  310. EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
  311. null /* rely on endpointer default */);
  312. mSpeechRecognizer.startListening(intent);
  313. }
  314. /**
  315. * Gets the value of the provided Gservices key, attempts to parse it into a long,
  316. * and if successful, puts the long value as an extra in the provided intent.
  317. */
  318. private void putEndpointerExtra(ContentResolver cr, Intent i,
  319. String gservicesKey, String intentExtraKey, String defaultValue) {
  320. long l = -1;
  321. String s = SettingsUtil.getSettingsString(cr, gservicesKey, defaultValue);
  322. if (s != null) {
  323. try {
  324. l = Long.valueOf(s);
  325. } catch (NumberFormatException e) {
  326. Log.e(TAG, "could not parse value for " + gservicesKey + ": " + s);
  327. }
  328. }
  329. if (l != -1) i.putExtra(intentExtraKey, l);
  330. }
  331. public void destroy() {
  332. mSpeechRecognizer.destroy();
  333. }
  334. /**
  335. * Creates a new instance of the view that is returned by {@link #getView()}
  336. * Clients should use this when a previously returned view is stuck in a
  337. * layout that is being thrown away and a new one is need to show to the
  338. * user.
  339. */
  340. public void newView() {
  341. mRecognitionView = new RecognitionView(mContext, this);
  342. }
  343. /**
  344. * @return a view that shows the recognition flow--e.g., "Speak now" and
  345. * "working" dialogs.
  346. */
  347. public View getView() {
  348. return mRecognitionView.getView();
  349. }
  350. /**
  351. * Handle the cancel button.
  352. */
  353. public void onClick(View view) {
  354. if (view.getId() == R.id.button) {
  355. cancel();
  356. }
  357. }
  358. public void logTextModifiedByTypingInsertion(int length) {
  359. mLogger.textModifiedByTypingInsertion(length);
  360. }
  361. public void logTextModifiedByTypingInsertionPunctuation(int length) {
  362. mLogger.textModifiedByTypingInsertionPunctuation(length);
  363. }
  364. public void logTextModifiedByTypingDeletion(int length) {
  365. mLogger.textModifiedByTypingDeletion(length);
  366. }
  367. public void logTextModifiedByChooseSuggestion(String suggestion, int index,
  368. String wordSeparators, InputConnection ic) {
  369. EditingUtil.Range range = new EditingUtil.Range();
  370. String wordToBeReplaced = EditingUtil.getWordAtCursor(ic, wordSeparators, range);
  371. // If we enable phrase-based alternatives, only send up the first word
  372. // in suggestion and wordToBeReplaced.
  373. mLogger.textModifiedByChooseSuggestion(suggestion.length(), wordToBeReplaced.length(),
  374. index, wordToBeReplaced, suggestion);
  375. }
  376. public void logKeyboardWarningDialogShown() {
  377. mLogger.keyboardWarningDialogShown();
  378. }
  379. public void logKeyboardWarningDialogDismissed() {
  380. mLogger.keyboardWarningDialogDismissed();
  381. }
  382. public void logKeyboardWarningDialogOk() {
  383. mLogger.keyboardWarningDialogOk();
  384. }
  385. public void logKeyboardWarningDialogCancel() {
  386. mLogger.keyboardWarningDialogCancel();
  387. }
  388. public void logSwipeHintDisplayed() {
  389. mLogger.swipeHintDisplayed();
  390. }
  391. public void logPunctuationHintDisplayed() {
  392. mLogger.punctuationHintDisplayed();
  393. }
  394. public void logVoiceInputDelivered(int length) {
  395. mLogger.voiceInputDelivered(length);
  396. }
  397. public void logInputEnded() {
  398. mLogger.inputEnded();
  399. }
  400. public void flushLogs() {
  401. mLogger.flush();
  402. }
  403. private static Intent makeIntent() {
  404. Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
  405. // On Cupcake, use VoiceIMEHelper since VoiceSearch doesn't support.
  406. // On Donut, always use VoiceSearch, since VoiceIMEHelper and
  407. // VoiceSearch may conflict.
  408. if (Build.VERSION.RELEASE.equals("1.5")) {
  409. intent = intent.setClassName(
  410. "com.google.android.voiceservice",
  411. "com.google.android.voiceservice.IMERecognitionService");
  412. } else {
  413. intent = intent.setClassName(
  414. "com.google.android.voicesearch",
  415. "com.google.android.voicesearch.RecognitionService");
  416. }
  417. return intent;
  418. }
  419. /**
  420. * Cancel in-progress speech recognition.
  421. */
  422. public void cancel() {
  423. switch (mState) {
  424. case LISTENING:
  425. mLogger.cancelDuringListening();
  426. break;
  427. case WORKING:
  428. mLogger.cancelDuringWorking();
  429. break;
  430. case ERROR:
  431. mLogger.cancelDuringError();
  432. break;
  433. }
  434. mState = DEFAULT;
  435. // Remove all pending tasks (e.g., timers to cancel voice input)
  436. mHandler.removeMessages(MSG_CLOSE_ERROR_DIALOG);
  437. mSpeechRecognizer.cancel();
  438. mUiListener.onCancelVoice();
  439. mRecognitionView.finish();
  440. }
  441. private int getErrorStringId(int errorType, boolean endpointed) {
  442. switch (errorType) {
  443. // We use CLIENT_ERROR to signify that voice search is not available on the device.
  444. case SpeechRecognizer.ERROR_CLIENT:
  445. return R.string.voice_not_installed;
  446. case SpeechRecognizer.ERROR_NETWORK:
  447. return R.string.voice_network_error;
  448. case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
  449. return endpointed ?
  450. R.string.voice_network_error : R.string.voice_too_much_speech;
  451. case SpeechRecognizer.ERROR_AUDIO:
  452. return R.string.voice_audio_error;
  453. case SpeechRecognizer.ERROR_SERVER:
  454. return R.string.voice_server_error;
  455. case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
  456. return R.string.voice_speech_timeout;
  457. case SpeechRecognizer.ERROR_NO_MATCH:
  458. return R.string.voice_no_match;
  459. default: return R.string.voice_error;
  460. }
  461. }
  462. private void onError(int errorType, boolean endpointed) {
  463. Log.i(TAG, "error " + errorType);
  464. mLogger.error(errorType);
  465. onError(mContext.getString(getErrorStringId(errorType, endpointed)));
  466. }
  467. private void onError(String error) {
  468. mState = ERROR;
  469. mRecognitionView.showError(error);
  470. // Wait a couple seconds and then automatically dismiss message.
  471. mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_CLOSE_ERROR_DIALOG), 2000);
  472. }
  473. private class ImeRecognitionListener implements RecognitionListener {
  474. // Waveform data
  475. final ByteArrayOutputStream mWaveBuffer = new ByteArrayOutputStream();
  476. int mSpeechStart;
  477. private boolean mEndpointed = false;
  478. public void onReadyForSpeech(Bundle noiseParams) {
  479. mRecognitionView.showListening();
  480. }
  481. public void onBeginningOfSpeech() {
  482. mEndpointed = false;
  483. mSpeechStart = mWaveBuffer.size();
  484. }
  485. public void onRmsChanged(float rmsdB) {
  486. mRecognitionView.updateVoiceMeter(rmsdB);
  487. }
  488. public void onBufferReceived(byte[] buf) {
  489. try {
  490. mWaveBuffer.write(buf);
  491. } catch (IOException e) {}
  492. }
  493. public void onEndOfSpeech() {
  494. mEndpointed = true;
  495. mState = WORKING;
  496. mRecognitionView.showWorking(mWaveBuffer, mSpeechStart, mWaveBuffer.size());
  497. }
  498. public void onError(int errorType) {
  499. mState = ERROR;
  500. VoiceInput.this.onError(errorType, mEndpointed);
  501. }
  502. public void onResults(Bundle resultsBundle) {
  503. List<String> results = resultsBundle
  504. .getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
  505. // VS Market update is needed for IME froyo clients to access the alternatesBundle
  506. // TODO: verify this.
  507. Bundle alternatesBundle = resultsBundle.getBundle(ALTERNATES_BUNDLE);
  508. mState = DEFAULT;
  509. final Map<String, List<CharSequence>> alternatives =
  510. new HashMap<String, List<CharSequence>>();
  511. if (ENABLE_WORD_CORRECTIONS && alternatesBundle != null && results.size() > 0) {
  512. // Use the top recognition result to map each alternative's start:length to a word.
  513. String[] words = results.get(0).split(" ");
  514. Bundle spansBundle = alternatesBundle.getBundle(AlternatesBundleKeys.SPANS);
  515. for (String key : spansBundle.keySet()) {
  516. // Get the word for which these alternates correspond to.
  517. Bundle spanBundle = spansBundle.getBundle(key);
  518. int start = spanBundle.getInt(AlternatesBundleKeys.START);
  519. int length = spanBundle.getInt(AlternatesBundleKeys.LENGTH);
  520. // Only keep single-word based alternatives.
  521. if (length == 1 && start < words.length) {
  522. // Get the alternatives associated with the span.
  523. // If a word appears twice in a recognition result,
  524. // concatenate the alternatives for the word.
  525. List<CharSequence> altList = alternatives.get(words[start]);
  526. if (altList == null) {
  527. altList = new ArrayList<CharSequence>();
  528. alternatives.put(words[start], altList);
  529. }
  530. Parcelable[] alternatesArr = spanBundle
  531. .getParcelableArray(AlternatesBundleKeys.ALTERNATES);
  532. for (int j = 0; j < alternatesArr.length &&
  533. altList.size() < MAX_ALT_LIST_LENGTH; j++) {
  534. Bundle alternateBundle = (Bundle) alternatesArr[j];
  535. String alternate = alternateBundle.getString(AlternatesBundleKeys.TEXT);
  536. // Don't allow duplicates in the alternates list.
  537. if (!altList.contains(alternate)) {
  538. altList.add(alternate);
  539. }
  540. }
  541. }
  542. }
  543. }
  544. if (results.size() > 5) {
  545. results = results.subList(0, 5);
  546. }
  547. mUiListener.onVoiceResults(results, alternatives);
  548. mRecognitionView.finish();
  549. }
  550. public void onPartialResults(final Bundle partialResults) {
  551. // currently - do nothing
  552. }
  553. public void onEvent(int eventType, Bundle params) {
  554. // do nothing - reserved for events that might be added in the future
  555. }
  556. }
  557. }