Adding VoiceIME support for new RecognitionManager interface
Change-Id: I14506149def2f5b47fa2697aef49ff5cd41b64a8main
parent
359f168161
commit
16668d952f
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin;
|
package com.android.inputmethod.latin;
|
||||||
|
|
||||||
import com.google.android.collect.Lists;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.backup.BackupManager;
|
import android.backup.BackupManager;
|
||||||
|
@ -25,16 +23,17 @@ import android.content.DialogInterface;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.CheckBoxPreference;
|
import android.preference.CheckBoxPreference;
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceActivity;
|
import android.preference.PreferenceActivity;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.Preference.OnPreferenceClickListener;
|
import android.preference.Preference.OnPreferenceClickListener;
|
||||||
|
import android.speech.RecognitionManager;
|
||||||
import android.text.AutoText;
|
import android.text.AutoText;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.android.collect.Lists;
|
||||||
|
|
||||||
import com.android.inputmethod.voice.GoogleSettingsUtil;
|
import com.android.inputmethod.voice.GoogleSettingsUtil;
|
||||||
import com.android.inputmethod.voice.VoiceInput;
|
|
||||||
import com.android.inputmethod.voice.VoiceInputLogger;
|
import com.android.inputmethod.voice.VoiceInputLogger;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -93,7 +92,7 @@ public class LatinIMESettings extends PreferenceActivity
|
||||||
mShowSuggestions.setDependency(QUICK_FIXES_KEY);
|
mShowSuggestions.setDependency(QUICK_FIXES_KEY);
|
||||||
}
|
}
|
||||||
if (!LatinIME.VOICE_INSTALLED
|
if (!LatinIME.VOICE_INSTALLED
|
||||||
|| !VoiceInput.voiceIsAvailable(this)) {
|
|| !RecognitionManager.isRecognitionAvailable(this)) {
|
||||||
getPreferenceScreen().removePreference(mVoicePreference);
|
getPreferenceScreen().removePreference(mVoicePreference);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,21 +16,18 @@
|
||||||
|
|
||||||
package com.android.inputmethod.voice;
|
package com.android.inputmethod.voice;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.speech.RecognitionListener;
|
||||||
import android.os.RemoteException;
|
import android.speech.RecognitionManager;
|
||||||
import android.util.Log;
|
|
||||||
import android.speech.IRecognitionListener;
|
|
||||||
import android.speech.RecognitionServiceUtil;
|
|
||||||
import android.speech.RecognizerIntent;
|
import android.speech.RecognizerIntent;
|
||||||
import android.speech.RecognitionResult;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
|
|
||||||
import com.android.inputmethod.latin.R;
|
import com.android.inputmethod.latin.R;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -68,8 +65,6 @@ public class VoiceInput implements OnClickListener {
|
||||||
// landscape view. It causes Extracted text updates to be rejected due to a token mismatch
|
// landscape view. It causes Extracted text updates to be rejected due to a token mismatch
|
||||||
public static boolean ENABLE_WORD_CORRECTIONS = false;
|
public static boolean ENABLE_WORD_CORRECTIONS = false;
|
||||||
|
|
||||||
private static Boolean sVoiceIsAvailable = null;
|
|
||||||
|
|
||||||
// Dummy word suggestion which means "delete current word"
|
// Dummy word suggestion which means "delete current word"
|
||||||
public static final String DELETE_SYMBOL = " \u00D7 "; // times symbol
|
public static final String DELETE_SYMBOL = " \u00D7 "; // times symbol
|
||||||
|
|
||||||
|
@ -101,6 +96,9 @@ public class VoiceInput implements OnClickListener {
|
||||||
|
|
||||||
private int mState = DEFAULT;
|
private int mState = DEFAULT;
|
||||||
|
|
||||||
|
// flag that maintains the status of the RecognitionManager
|
||||||
|
private boolean mIsRecognitionInitialized = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events relating to the recognition UI. You must implement these.
|
* Events relating to the recognition UI. You must implement these.
|
||||||
*/
|
*/
|
||||||
|
@ -120,26 +118,29 @@ public class VoiceInput implements OnClickListener {
|
||||||
public void onCancelVoice();
|
public void onCancelVoice();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RecognitionServiceUtil.Connection mRecognitionConnection;
|
private RecognitionManager mRecognitionManager;
|
||||||
private IRecognitionListener mRecognitionListener;
|
private RecognitionListener mRecognitionListener;
|
||||||
private RecognitionView mRecognitionView;
|
private RecognitionView mRecognitionView;
|
||||||
private UiListener mUiListener;
|
private UiListener mUiListener;
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private ScheduledThreadPoolExecutor mExecutor;
|
private ScheduledThreadPoolExecutor mExecutor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context the service or activity in which we're runing.
|
* Context with which {@link RecognitionManager#startListening(Intent)} was
|
||||||
|
* executed. Used to store the context in case that startLitening was
|
||||||
|
* executed before the recognition service initialization was completed
|
||||||
|
*/
|
||||||
|
private FieldContext mStartListeningContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the service or activity in which we're running.
|
||||||
* @param uiHandler object to receive events from VoiceInput.
|
* @param uiHandler object to receive events from VoiceInput.
|
||||||
*/
|
*/
|
||||||
public VoiceInput(Context context, UiListener uiHandler) {
|
public VoiceInput(Context context, UiListener uiHandler) {
|
||||||
mLogger = VoiceInputLogger.getLogger(context);
|
mLogger = VoiceInputLogger.getLogger(context);
|
||||||
mRecognitionListener = new IMERecognitionListener();
|
mRecognitionListener = new ImeRecognitionListener();
|
||||||
mRecognitionConnection = new RecognitionServiceUtil.Connection() {
|
mRecognitionManager = RecognitionManager.createRecognitionManager(context,
|
||||||
public synchronized void onServiceConnected(
|
mRecognitionListener, new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH));
|
||||||
ComponentName name, IBinder service) {
|
|
||||||
super.onServiceConnected(name, service);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mUiListener = uiHandler;
|
mUiListener = uiHandler;
|
||||||
mContext = context;
|
mContext = context;
|
||||||
newView();
|
newView();
|
||||||
|
@ -158,7 +159,6 @@ public class VoiceInput implements OnClickListener {
|
||||||
mBlacklist.addApp("com.android.setupwizard");
|
mBlacklist.addApp("com.android.setupwizard");
|
||||||
|
|
||||||
mExecutor = new ScheduledThreadPoolExecutor(1);
|
mExecutor = new ScheduledThreadPoolExecutor(1);
|
||||||
bindIfNecessary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -177,23 +177,6 @@ public class VoiceInput implements OnClickListener {
|
||||||
return mRecommendedList.matches(context);
|
return mRecommendedList.matches(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if the speech service is available on the platform.
|
|
||||||
*/
|
|
||||||
public static boolean voiceIsAvailable(Context context) {
|
|
||||||
if (sVoiceIsAvailable != null) {
|
|
||||||
return sVoiceIsAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecognitionServiceUtil.Connection recognitionConnection =
|
|
||||||
new RecognitionServiceUtil.Connection();
|
|
||||||
boolean bound = context.bindService(
|
|
||||||
makeIntent(), recognitionConnection, Context.BIND_AUTO_CREATE);
|
|
||||||
context.unbindService(recognitionConnection);
|
|
||||||
sVoiceIsAvailable = bound;
|
|
||||||
return bound;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening for speech from the user. This will grab the microphone
|
* Start listening for speech from the user. This will grab the microphone
|
||||||
* and start updating the view provided by getView(). It is the caller's
|
* and start updating the view provided by getView(). It is the caller's
|
||||||
|
@ -212,63 +195,56 @@ public class VoiceInput implements OnClickListener {
|
||||||
|
|
||||||
mState = LISTENING;
|
mState = LISTENING;
|
||||||
|
|
||||||
if (mRecognitionConnection.mService == null) {
|
if (!mIsRecognitionInitialized) {
|
||||||
mRecognitionView.showInitializing();
|
mRecognitionView.showInitializing();
|
||||||
|
mStartListeningContext = context;
|
||||||
} else {
|
} else {
|
||||||
mRecognitionView.showStartState();
|
mRecognitionView.showStartState();
|
||||||
|
startListeningAfterInitialization(context);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!bindIfNecessary()) {
|
/**
|
||||||
mState = ERROR;
|
* Called only when the recognition manager's initialization completed
|
||||||
|
*
|
||||||
|
* @param context context with which {@link #startListening(FieldContext, boolean)} was executed
|
||||||
|
*/
|
||||||
|
private void startListeningAfterInitialization(FieldContext context) {
|
||||||
|
Intent intent = makeIntent();
|
||||||
|
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, "");
|
||||||
|
intent.putExtra(EXTRA_RECOGNITION_CONTEXT, context.getBundle());
|
||||||
|
intent.putExtra(EXTRA_CALLING_PACKAGE, "VoiceIME");
|
||||||
|
intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,
|
||||||
|
GoogleSettingsUtil.getGservicesInt(
|
||||||
|
mContext.getContentResolver(),
|
||||||
|
GoogleSettingsUtil.LATIN_IME_MAX_VOICE_RESULTS,
|
||||||
|
1));
|
||||||
|
|
||||||
// We use CLIENT_ERROR to signify voice search is not available on the device.
|
// Get endpointer params from Gservices.
|
||||||
onError(RecognitionResult.CLIENT_ERROR, false);
|
// TODO: Consider caching these values for improved performance on slower devices.
|
||||||
cancel();
|
final ContentResolver cr = mContext.getContentResolver();
|
||||||
}
|
putEndpointerExtra(
|
||||||
|
cr,
|
||||||
|
intent,
|
||||||
|
GoogleSettingsUtil.LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS,
|
||||||
|
EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS,
|
||||||
|
null /* rely on endpointer default */);
|
||||||
|
putEndpointerExtra(
|
||||||
|
cr,
|
||||||
|
intent,
|
||||||
|
GoogleSettingsUtil.LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
|
||||||
|
EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
|
||||||
|
INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS
|
||||||
|
/* our default value is different from the endpointer's */);
|
||||||
|
putEndpointerExtra(
|
||||||
|
cr,
|
||||||
|
intent,
|
||||||
|
GoogleSettingsUtil.
|
||||||
|
LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
|
||||||
|
EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
|
||||||
|
null /* rely on endpointer default */);
|
||||||
|
|
||||||
if (mRecognitionConnection.mService != null) {
|
mRecognitionManager.startListening(intent);
|
||||||
try {
|
|
||||||
Intent intent = makeIntent();
|
|
||||||
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, "");
|
|
||||||
intent.putExtra(EXTRA_RECOGNITION_CONTEXT, context.getBundle());
|
|
||||||
intent.putExtra(EXTRA_CALLING_PACKAGE, "VoiceIME");
|
|
||||||
intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,
|
|
||||||
GoogleSettingsUtil.getGservicesInt(
|
|
||||||
mContext.getContentResolver(),
|
|
||||||
GoogleSettingsUtil.LATIN_IME_MAX_VOICE_RESULTS,
|
|
||||||
1));
|
|
||||||
|
|
||||||
// Get endpointer params from Gservices.
|
|
||||||
// TODO: Consider caching these values for improved performance on slower devices.
|
|
||||||
ContentResolver cr = mContext.getContentResolver();
|
|
||||||
putEndpointerExtra(
|
|
||||||
cr,
|
|
||||||
intent,
|
|
||||||
GoogleSettingsUtil.LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS,
|
|
||||||
EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS,
|
|
||||||
null /* rely on endpointer default */);
|
|
||||||
putEndpointerExtra(
|
|
||||||
cr,
|
|
||||||
intent,
|
|
||||||
GoogleSettingsUtil.LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
|
|
||||||
EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
|
|
||||||
INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS
|
|
||||||
/* our default value is different from the endpointer's */);
|
|
||||||
putEndpointerExtra(
|
|
||||||
cr,
|
|
||||||
intent,
|
|
||||||
GoogleSettingsUtil.
|
|
||||||
LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
|
|
||||||
EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
|
|
||||||
null /* rely on endpointer default */);
|
|
||||||
|
|
||||||
mRecognitionConnection.mService.startListening(
|
|
||||||
intent, mRecognitionListener);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.e(TAG, "Could not start listening", e);
|
|
||||||
onError(-1 /* no specific error, just show default error */, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -291,9 +267,7 @@ public class VoiceInput implements OnClickListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
if (mRecognitionConnection.mService != null) {
|
mRecognitionManager.destroy();
|
||||||
//mContext.unbindService(mRecognitionConnection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -388,19 +362,6 @@ public class VoiceInput implements OnClickListener {
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind to the recognition service if necessary.
|
|
||||||
* @return true if we are bound or binding to the service, false if
|
|
||||||
* the recognition service is unavailable.
|
|
||||||
*/
|
|
||||||
private boolean bindIfNecessary() {
|
|
||||||
if (mRecognitionConnection.mService != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return mContext.bindService(
|
|
||||||
makeIntent(), mRecognitionConnection, Context.BIND_AUTO_CREATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel in-progress speech recognition.
|
* Cancel in-progress speech recognition.
|
||||||
*/
|
*/
|
||||||
|
@ -423,13 +384,7 @@ public class VoiceInput implements OnClickListener {
|
||||||
mExecutor.remove(runnable);
|
mExecutor.remove(runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mRecognitionConnection.mService != null) {
|
mRecognitionManager.cancel();
|
||||||
try {
|
|
||||||
mRecognitionConnection.mService.cancel();
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.e(TAG, "Exception on cancel", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mUiListener.onCancelVoice();
|
mUiListener.onCancelVoice();
|
||||||
mRecognitionView.finish();
|
mRecognitionView.finish();
|
||||||
}
|
}
|
||||||
|
@ -437,20 +392,20 @@ public class VoiceInput implements OnClickListener {
|
||||||
private int getErrorStringId(int errorType, boolean endpointed) {
|
private int getErrorStringId(int errorType, boolean endpointed) {
|
||||||
switch (errorType) {
|
switch (errorType) {
|
||||||
// We use CLIENT_ERROR to signify that voice search is not available on the device.
|
// We use CLIENT_ERROR to signify that voice search is not available on the device.
|
||||||
case RecognitionResult.CLIENT_ERROR:
|
case RecognitionManager.CLIENT_ERROR:
|
||||||
return R.string.voice_not_installed;
|
return R.string.voice_not_installed;
|
||||||
case RecognitionResult.NETWORK_ERROR:
|
case RecognitionManager.NETWORK_ERROR:
|
||||||
return R.string.voice_network_error;
|
return R.string.voice_network_error;
|
||||||
case RecognitionResult.NETWORK_TIMEOUT:
|
case RecognitionManager.NETWORK_TIMEOUT_ERROR:
|
||||||
return endpointed ?
|
return endpointed ?
|
||||||
R.string.voice_network_error : R.string.voice_too_much_speech;
|
R.string.voice_network_error : R.string.voice_too_much_speech;
|
||||||
case RecognitionResult.AUDIO_ERROR:
|
case RecognitionManager.AUDIO_ERROR:
|
||||||
return R.string.voice_audio_error;
|
return R.string.voice_audio_error;
|
||||||
case RecognitionResult.SERVER_ERROR:
|
case RecognitionManager.SERVER_ERROR:
|
||||||
return R.string.voice_server_error;
|
return R.string.voice_server_error;
|
||||||
case RecognitionResult.SPEECH_TIMEOUT:
|
case RecognitionManager.SPEECH_TIMEOUT_ERROR:
|
||||||
return R.string.voice_speech_timeout;
|
return R.string.voice_speech_timeout;
|
||||||
case RecognitionResult.NO_MATCH:
|
case RecognitionManager.NO_MATCH_ERROR:
|
||||||
return R.string.voice_no_match;
|
return R.string.voice_no_match;
|
||||||
default: return R.string.voice_error;
|
default: return R.string.voice_error;
|
||||||
}
|
}
|
||||||
|
@ -472,7 +427,7 @@ public class VoiceInput implements OnClickListener {
|
||||||
}}, 2000, TimeUnit.MILLISECONDS);
|
}}, 2000, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class IMERecognitionListener extends IRecognitionListener.Stub {
|
private class ImeRecognitionListener implements RecognitionListener {
|
||||||
// Waveform data
|
// Waveform data
|
||||||
final ByteArrayOutputStream mWaveBuffer = new ByteArrayOutputStream();
|
final ByteArrayOutputStream mWaveBuffer = new ByteArrayOutputStream();
|
||||||
int mSpeechStart;
|
int mSpeechStart;
|
||||||
|
@ -508,19 +463,17 @@ public class VoiceInput implements OnClickListener {
|
||||||
VoiceInput.this.onError(errorType, mEndpointed);
|
VoiceInput.this.onError(errorType, mEndpointed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onResults(List<RecognitionResult> results, long token) {
|
public void onResults(Bundle resultsBundle) {
|
||||||
|
List<String> results = resultsBundle
|
||||||
|
.getStringArrayList(RecognitionManager.RECOGNITION_RESULTS_STRING_ARRAY);
|
||||||
mState = DEFAULT;
|
mState = DEFAULT;
|
||||||
List<String> resultsAsText = new ArrayList<String>();
|
|
||||||
for (RecognitionResult result : results) {
|
|
||||||
resultsAsText.add(result.mText);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, List<CharSequence>> alternatives =
|
final Map<String, List<CharSequence>> alternatives =
|
||||||
new HashMap<String, List<CharSequence>>();
|
new HashMap<String, List<CharSequence>>();
|
||||||
if (resultsAsText.size() >= 2 && ENABLE_WORD_CORRECTIONS) {
|
if (results.size() >= 2 && ENABLE_WORD_CORRECTIONS) {
|
||||||
String[][] words = new String[resultsAsText.size()][];
|
final String[][] words = new String[results.size()][];
|
||||||
for (int i = 0; i < words.length; i++) {
|
for (int i = 0; i < words.length; i++) {
|
||||||
words[i] = resultsAsText.get(i).split(" ");
|
words[i] = results.get(i).split(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int key = 0; key < words[0].length; key++) {
|
for (int key = 0; key < words[0].length; key++) {
|
||||||
|
@ -539,13 +492,22 @@ public class VoiceInput implements OnClickListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultsAsText.size() > 5) {
|
if (results.size() > 5) {
|
||||||
resultsAsText = resultsAsText.subList(0, 5);
|
results = results.subList(0, 5);
|
||||||
}
|
}
|
||||||
mUiListener.onVoiceResults(resultsAsText, alternatives);
|
mUiListener.onVoiceResults(results, alternatives);
|
||||||
mRecognitionView.finish();
|
mRecognitionView.finish();
|
||||||
|
}
|
||||||
|
|
||||||
destroy();
|
public void onInit() {
|
||||||
|
mIsRecognitionInitialized = true;
|
||||||
|
if (mState == LISTENING) {
|
||||||
|
startListeningAfterInitialization(mStartListeningContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPartialResults(final Bundle partialResults) {
|
||||||
|
// TODO To add partial results as user speaks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue