aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/deprecated
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/deprecated')
-rw-r--r--java/src/com/android/inputmethod/deprecated/LanguageSwitcherProxy.java90
-rw-r--r--java/src/com/android/inputmethod/deprecated/VoiceProxy.java842
-rw-r--r--java/src/com/android/inputmethod/deprecated/compat/VoiceInputLoggerCompatUtils.java36
-rw-r--r--java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java255
-rw-r--r--java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java234
-rw-r--r--java/src/com/android/inputmethod/deprecated/recorrection/Recorrection.java287
-rw-r--r--java/src/com/android/inputmethod/deprecated/recorrection/RecorrectionSuggestionEntries.java62
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/FieldContext.java104
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/Hints.java188
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java354
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java110
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java155
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java685
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java266
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java92
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/Whitelist.java68
16 files changed, 3828 insertions, 0 deletions
diff --git a/java/src/com/android/inputmethod/deprecated/LanguageSwitcherProxy.java b/java/src/com/android/inputmethod/deprecated/LanguageSwitcherProxy.java
new file mode 100644
index 000000000..290e6b554
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/LanguageSwitcherProxy.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated;
+
+import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.deprecated.languageswitcher.LanguageSwitcher;
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.Settings;
+
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+
+import java.util.Locale;
+
+// This class is used only when the IME doesn't use method.xml for language switching.
+public class LanguageSwitcherProxy implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final LanguageSwitcherProxy sInstance = new LanguageSwitcherProxy();
+ private LatinIME mService;
+ private LanguageSwitcher mLanguageSwitcher;
+ private SharedPreferences mPrefs;
+
+ public static LanguageSwitcherProxy getInstance() {
+ if (InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) return null;
+ return sInstance;
+ }
+
+ public static void init(LatinIME service, SharedPreferences prefs) {
+ if (InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) return;
+ final Configuration conf = service.getResources().getConfiguration();
+ sInstance.mLanguageSwitcher = new LanguageSwitcher(service);
+ sInstance.mLanguageSwitcher.loadLocales(prefs, conf.locale);
+ sInstance.mPrefs = prefs;
+ sInstance.mService = service;
+ prefs.registerOnSharedPreferenceChangeListener(sInstance);
+ }
+
+ public static void onConfigurationChanged(Configuration conf) {
+ if (InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) return;
+ sInstance.mLanguageSwitcher.onConfigurationChanged(conf, sInstance.mPrefs);
+ }
+
+ public static void loadSettings() {
+ if (InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) return;
+ sInstance.mLanguageSwitcher.loadLocales(sInstance.mPrefs, null);
+ }
+
+ public int getLocaleCount() {
+ return mLanguageSwitcher.getLocaleCount();
+ }
+
+ public String[] getEnabledLanguages(boolean allowImplicitlySelectedLanguages) {
+ return mLanguageSwitcher.getEnabledLanguages(allowImplicitlySelectedLanguages);
+ }
+
+ public Locale getInputLocale() {
+ return mLanguageSwitcher.getInputLocale();
+ }
+
+ public void setLocale(String localeStr) {
+ mLanguageSwitcher.setLocale(localeStr);
+ mLanguageSwitcher.persist(mPrefs);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ // PREF_SELECTED_LANGUAGES: enabled input subtypes
+ // PREF_INPUT_LANGUAGE: current input subtype
+ if (key.equals(Settings.PREF_SELECTED_LANGUAGES)
+ || key.equals(Settings.PREF_INPUT_LANGUAGE)) {
+ mLanguageSwitcher.loadLocales(prefs, null);
+ if (mService != null) {
+ mService.onRefreshKeyboard();
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/VoiceProxy.java b/java/src/com/android/inputmethod/deprecated/VoiceProxy.java
new file mode 100644
index 000000000..85993ea4d
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/VoiceProxy.java
@@ -0,0 +1,842 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated;
+
+import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.deprecated.voice.FieldContext;
+import com.android.inputmethod.deprecated.voice.Hints;
+import com.android.inputmethod.deprecated.voice.SettingsUtil;
+import com.android.inputmethod.deprecated.voice.VoiceInput;
+import com.android.inputmethod.deprecated.voice.VoiceInputLogger;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.latin.EditingUtils;
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.LatinIME.UIHandler;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SharedPreferencesCompat;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.Utils;
+
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.provider.Browser;
+import android.speech.SpeechRecognizer;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.text.style.URLSpan;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class VoiceProxy implements VoiceInput.UiListener {
+ private static final VoiceProxy sInstance = new VoiceProxy();
+
+ public static final boolean VOICE_INSTALLED = true;
+ private static final boolean ENABLE_VOICE_BUTTON = true;
+ private static final String PREF_VOICE_MODE = "voice_mode";
+ // Whether or not the user has used voice input before (and thus, whether to show the
+ // first-run warning dialog or not).
+ private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input";
+ // Whether or not the user has used voice input from an unsupported locale UI before.
+ // For example, the user has a Chinese UI but activates voice input.
+ private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE =
+ "has_used_voice_input_unsupported_locale";
+ private static final int RECOGNITIONVIEW_HEIGHT_THRESHOLD_RATIO = 6;
+ // TODO: Adjusted on phones for now
+ private static final int RECOGNITIONVIEW_MINIMUM_HEIGHT_DIP = 244;
+
+ private static final String TAG = VoiceProxy.class.getSimpleName();
+ private static final boolean DEBUG = LatinImeLogger.sDBG;
+
+ private boolean mAfterVoiceInput;
+ private boolean mHasUsedVoiceInput;
+ private boolean mHasUsedVoiceInputUnsupportedLocale;
+ private boolean mImmediatelyAfterVoiceInput;
+ private boolean mIsShowingHint;
+ private boolean mLocaleSupportedForVoiceInput;
+ private boolean mPasswordText;
+ private boolean mRecognizing;
+ private boolean mShowingVoiceSuggestions;
+ private boolean mVoiceButtonEnabled;
+ private boolean mVoiceButtonOnPrimary;
+ private boolean mVoiceInputHighlighted;
+
+ private int mMinimumVoiceRecognitionViewHeightPixel;
+ private InputMethodManagerCompatWrapper mImm;
+ private LatinIME mService;
+ private AlertDialog mVoiceWarningDialog;
+ private VoiceInput mVoiceInput;
+ private final VoiceResults mVoiceResults = new VoiceResults();
+ private Hints mHints;
+ private UIHandler mHandler;
+ private SubtypeSwitcher mSubtypeSwitcher;
+
+ // For each word, a list of potential replacements, usually from voice.
+ private final Map<String, List<CharSequence>> mWordToSuggestions =
+ new HashMap<String, List<CharSequence>>();
+
+ public static VoiceProxy init(LatinIME context, SharedPreferences prefs, UIHandler h) {
+ sInstance.initInternal(context, prefs, h);
+ return sInstance;
+ }
+
+ public static VoiceProxy getInstance() {
+ return sInstance;
+ }
+
+ private void initInternal(LatinIME service, SharedPreferences prefs, UIHandler h) {
+ mService = service;
+ mHandler = h;
+ mMinimumVoiceRecognitionViewHeightPixel = Utils.dipToPixel(
+ Utils.getDipScale(service), RECOGNITIONVIEW_MINIMUM_HEIGHT_DIP);
+ mImm = InputMethodManagerCompatWrapper.getInstance(service);
+ mSubtypeSwitcher = SubtypeSwitcher.getInstance();
+ if (VOICE_INSTALLED) {
+ mVoiceInput = new VoiceInput(service, this);
+ mHints = new Hints(service, prefs, new Hints.Display() {
+ @Override
+ public void showHint(int viewResource) {
+ View view = LayoutInflater.from(mService).inflate(viewResource, null);
+// mService.setCandidatesView(view);
+// mService.setCandidatesViewShown(true);
+ mIsShowingHint = true;
+ }
+ });
+ }
+ }
+
+ private VoiceProxy() {
+ // Intentional empty constructor for singleton.
+ }
+
+ public void resetVoiceStates(boolean isPasswordText) {
+ mAfterVoiceInput = false;
+ mImmediatelyAfterVoiceInput = false;
+ mShowingVoiceSuggestions = false;
+ mVoiceInputHighlighted = false;
+ mPasswordText = isPasswordText;
+ }
+
+ public void flushVoiceInputLogs(boolean configurationChanged) {
+ if (VOICE_INSTALLED && !configurationChanged) {
+ if (mAfterVoiceInput) {
+ mVoiceInput.flushAllTextModificationCounters();
+ mVoiceInput.logInputEnded();
+ }
+ mVoiceInput.flushLogs();
+ mVoiceInput.cancel();
+ }
+ }
+
+ public void flushAndLogAllTextModificationCounters(int index, CharSequence suggestion,
+ String wordSeparators) {
+ if (mAfterVoiceInput && mShowingVoiceSuggestions) {
+ mVoiceInput.flushAllTextModificationCounters();
+ // send this intent AFTER logging any prior aggregated edits.
+ mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index,
+ wordSeparators, mService.getCurrentInputConnection());
+ }
+ }
+
+ private void showVoiceWarningDialog(final boolean swipe, IBinder token) {
+ if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
+ return;
+ }
+ AlertDialog.Builder builder = new UrlLinkAlertDialogBuilder(mService);
+ builder.setCancelable(true);
+ builder.setIcon(R.drawable.ic_mic_dialog);
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ mVoiceInput.logKeyboardWarningDialogOk();
+ reallyStartListening(swipe);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ mVoiceInput.logKeyboardWarningDialogCancel();
+ switchToLastInputMethod();
+ }
+ });
+ // When the dialog is dismissed by user's cancellation, switch back to the last input method
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface arg0) {
+ mVoiceInput.logKeyboardWarningDialogCancel();
+ switchToLastInputMethod();
+ }
+ });
+
+ final CharSequence message;
+ if (mLocaleSupportedForVoiceInput) {
+ message = TextUtils.concat(
+ mService.getText(R.string.voice_warning_may_not_understand), "\n\n",
+ mService.getText(R.string.voice_warning_how_to_turn_off));
+ } else {
+ message = TextUtils.concat(
+ mService.getText(R.string.voice_warning_locale_not_supported), "\n\n",
+ mService.getText(R.string.voice_warning_may_not_understand), "\n\n",
+ mService.getText(R.string.voice_warning_how_to_turn_off));
+ }
+ builder.setMessage(message);
+ builder.setTitle(R.string.voice_warning_title);
+ mVoiceWarningDialog = builder.create();
+ final Window window = mVoiceWarningDialog.getWindow();
+ final WindowManager.LayoutParams lp = window.getAttributes();
+ lp.token = token;
+ lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+ window.setAttributes(lp);
+ window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+ mVoiceInput.logKeyboardWarningDialogShown();
+ mVoiceWarningDialog.show();
+ }
+
+ private static class UrlLinkAlertDialogBuilder extends AlertDialog.Builder {
+ private AlertDialog mAlertDialog;
+
+ public UrlLinkAlertDialogBuilder(Context context) {
+ super(context);
+ }
+
+ @Override
+ public AlertDialog.Builder setMessage(CharSequence message) {
+ return super.setMessage(replaceURLSpan(message));
+ }
+
+ private Spanned replaceURLSpan(CharSequence message) {
+ // Replace all spans with the custom span
+ final SpannableStringBuilder ssb = new SpannableStringBuilder(message);
+ for (URLSpan span : ssb.getSpans(0, ssb.length(), URLSpan.class)) {
+ int spanStart = ssb.getSpanStart(span);
+ int spanEnd = ssb.getSpanEnd(span);
+ int spanFlags = ssb.getSpanFlags(span);
+ ssb.removeSpan(span);
+ ssb.setSpan(new ClickableSpan(span.getURL()), spanStart, spanEnd, spanFlags);
+ }
+ return ssb;
+ }
+
+ @Override
+ public AlertDialog create() {
+ final AlertDialog dialog = super.create();
+
+ dialog.setOnShowListener(new DialogInterface.OnShowListener() {
+ @Override
+ public void onShow(DialogInterface dialogInterface) {
+ // Make URL in the dialog message click-able.
+ TextView textView = (TextView) mAlertDialog.findViewById(android.R.id.message);
+ if (textView != null) {
+ textView.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ });
+ mAlertDialog = dialog;
+ return dialog;
+ }
+
+ class ClickableSpan extends URLSpan {
+ public ClickableSpan(String url) {
+ super(url);
+ }
+
+ @Override
+ public void onClick(View widget) {
+ Uri uri = Uri.parse(getURL());
+ Context context = widget.getContext();
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ // Add this flag to start an activity from service
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
+ // Dismiss the warning dialog and go back to the previous IME.
+ // TODO: If we can find a way to bring the new activity to front while keeping
+ // the warning dialog, we don't need to dismiss it here.
+ mAlertDialog.cancel();
+ context.startActivity(intent);
+ }
+ }
+ }
+
+ public void showPunctuationHintIfNecessary() {
+ InputConnection ic = mService.getCurrentInputConnection();
+ if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) {
+ if (mHints.showPunctuationHintIfNecessary(ic)) {
+ mVoiceInput.logPunctuationHintDisplayed();
+ }
+ }
+ mImmediatelyAfterVoiceInput = false;
+ }
+
+ public void hideVoiceWindow(boolean configurationChanging) {
+ if (!configurationChanging) {
+ if (mAfterVoiceInput)
+ mVoiceInput.logInputEnded();
+ if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
+ mVoiceInput.logKeyboardWarningDialogDismissed();
+ mVoiceWarningDialog.dismiss();
+ mVoiceWarningDialog = null;
+ }
+ if (VOICE_INSTALLED & mRecognizing) {
+ mVoiceInput.cancel();
+ }
+ }
+ mWordToSuggestions.clear();
+ }
+
+ public void setCursorAndSelection(int newSelEnd, int newSelStart) {
+ if (mAfterVoiceInput) {
+ mVoiceInput.setCursorPos(newSelEnd);
+ mVoiceInput.setSelectionSpan(newSelEnd - newSelStart);
+ }
+ }
+
+ public void setVoiceInputHighlighted(boolean b) {
+ mVoiceInputHighlighted = b;
+ }
+
+ public void setShowingVoiceSuggestions(boolean b) {
+ mShowingVoiceSuggestions = b;
+ }
+
+ public boolean isVoiceButtonEnabled() {
+ return mVoiceButtonEnabled;
+ }
+
+ public boolean isVoiceButtonOnPrimary() {
+ return mVoiceButtonOnPrimary;
+ }
+
+ public boolean isVoiceInputHighlighted() {
+ return mVoiceInputHighlighted;
+ }
+
+ public boolean isRecognizing() {
+ return mRecognizing;
+ }
+
+ public boolean needsToShowWarningDialog() {
+ return !mHasUsedVoiceInput
+ || (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale);
+ }
+
+ public boolean getAndResetIsShowingHint() {
+ boolean ret = mIsShowingHint;
+ mIsShowingHint = false;
+ return ret;
+ }
+
+ private void revertVoiceInput() {
+ InputConnection ic = mService.getCurrentInputConnection();
+ if (ic != null) ic.commitText("", 1);
+ mService.updateSuggestions();
+ mVoiceInputHighlighted = false;
+ }
+
+ public void commitVoiceInput() {
+ if (VOICE_INSTALLED && mVoiceInputHighlighted) {
+ InputConnection ic = mService.getCurrentInputConnection();
+ if (ic != null) ic.finishComposingText();
+ mService.updateSuggestions();
+ mVoiceInputHighlighted = false;
+ }
+ }
+
+ public boolean logAndRevertVoiceInput() {
+ if (VOICE_INSTALLED && mVoiceInputHighlighted) {
+ mVoiceInput.incrementTextModificationDeleteCount(
+ mVoiceResults.candidates.get(0).toString().length());
+ revertVoiceInput();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void rememberReplacedWord(CharSequence suggestion,String wordSeparators) {
+ if (mShowingVoiceSuggestions) {
+ // Retain the replaced word in the alternatives array.
+ String wordToBeReplaced = EditingUtils.getWordAtCursor(
+ mService.getCurrentInputConnection(), wordSeparators);
+ if (!mWordToSuggestions.containsKey(wordToBeReplaced)) {
+ wordToBeReplaced = wordToBeReplaced.toLowerCase();
+ }
+ if (mWordToSuggestions.containsKey(wordToBeReplaced)) {
+ List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced);
+ if (suggestions.contains(suggestion)) {
+ suggestions.remove(suggestion);
+ }
+ suggestions.add(wordToBeReplaced);
+ mWordToSuggestions.remove(wordToBeReplaced);
+ mWordToSuggestions.put(suggestion.toString(), suggestions);
+ }
+ }
+ }
+
+ /**
+ * Tries to apply any voice alternatives for the word if this was a spoken word and
+ * there are voice alternatives.
+ * @param touching The word that the cursor is touching, with position information
+ * @return true if an alternative was found, false otherwise.
+ */
+ public boolean applyVoiceAlternatives(EditingUtils.SelectedWord touching) {
+ // Search for result in spoken word alternatives
+ String selectedWord = touching.mWord.toString().trim();
+ if (!mWordToSuggestions.containsKey(selectedWord)) {
+ selectedWord = selectedWord.toLowerCase();
+ }
+ if (mWordToSuggestions.containsKey(selectedWord)) {
+ mShowingVoiceSuggestions = true;
+ List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
+ SuggestedWords.Builder builder = new SuggestedWords.Builder();
+ // If the first letter of touching is capitalized, make all the suggestions
+ // start with a capital letter.
+ if (Character.isUpperCase(touching.mWord.charAt(0))) {
+ for (CharSequence word : suggestions) {
+ String str = word.toString();
+ word = Character.toUpperCase(str.charAt(0)) + str.substring(1);
+ builder.addWord(word);
+ }
+ } else {
+ builder.addWords(suggestions, null);
+ }
+ builder.setTypedWordValid(true).setHasMinimalSuggestion(true);
+ mService.setSuggestions(builder.build());
+// mService.setCandidatesViewShown(true);
+ return true;
+ }
+ return false;
+ }
+
+ public void handleBackspace() {
+ if (mAfterVoiceInput) {
+ // Don't log delete if the user is pressing delete at
+ // the beginning of the text box (hence not deleting anything)
+ if (mVoiceInput.getCursorPos() > 0) {
+ // If anything was selected before the delete was pressed, increment the
+ // delete count by the length of the selection
+ int deleteLen = mVoiceInput.getSelectionSpan() > 0 ?
+ mVoiceInput.getSelectionSpan() : 1;
+ mVoiceInput.incrementTextModificationDeleteCount(deleteLen);
+ }
+ }
+ }
+
+ public void handleCharacter() {
+ commitVoiceInput();
+ if (mAfterVoiceInput) {
+ // Assume input length is 1. This assumption fails for smiley face insertions.
+ mVoiceInput.incrementTextModificationInsertCount(1);
+ }
+ }
+
+ public void handleSeparator() {
+ commitVoiceInput();
+ if (mAfterVoiceInput){
+ // Assume input length is 1. This assumption fails for smiley face insertions.
+ mVoiceInput.incrementTextModificationInsertPunctuationCount(1);
+ }
+ }
+
+ public void handleClose() {
+ if (VOICE_INSTALLED & mRecognizing) {
+ mVoiceInput.cancel();
+ }
+ }
+
+
+ public void handleVoiceResults(boolean capitalizeFirstWord) {
+ mAfterVoiceInput = true;
+ mImmediatelyAfterVoiceInput = true;
+
+ InputConnection ic = mService.getCurrentInputConnection();
+ if (!mService.isFullscreenMode()) {
+ // Start listening for updates to the text from typing, etc.
+ if (ic != null) {
+ ExtractedTextRequest req = new ExtractedTextRequest();
+ ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR);
+ }
+ }
+ mService.vibrate();
+
+ final List<CharSequence> nBest = new ArrayList<CharSequence>();
+ for (String c : mVoiceResults.candidates) {
+ if (capitalizeFirstWord) {
+ c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length());
+ }
+ nBest.add(c);
+ }
+ if (nBest.size() == 0) {
+ return;
+ }
+ String bestResult = nBest.get(0).toString();
+ mVoiceInput.logVoiceInputDelivered(bestResult.length());
+ mHints.registerVoiceResult(bestResult);
+
+ if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text
+ mService.commitTyped(ic);
+ EditingUtils.appendText(ic, bestResult);
+ if (ic != null) ic.endBatchEdit();
+
+ mVoiceInputHighlighted = true;
+ mWordToSuggestions.putAll(mVoiceResults.alternatives);
+ onCancelVoice();
+ }
+
+ public void switchToRecognitionStatusView(final Configuration configuration) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+// mService.setCandidatesViewShown(false);
+ mRecognizing = true;
+ mVoiceInput.newView();
+ View v = mVoiceInput.getView();
+
+ ViewParent p = v.getParent();
+ if (p != null && p instanceof ViewGroup) {
+ ((ViewGroup) p).removeView(v);
+ }
+
+ View keyboardView = KeyboardSwitcher.getInstance().getKeyboardView();
+
+ // The full height of the keyboard is difficult to calculate
+ // as the dimension is expressed in "mm" and not in "pixel"
+ // As we add mm, we don't know how the rounding is going to work
+ // thus we may end up with few pixels extra (or less).
+ if (keyboardView != null) {
+ View popupLayout = v.findViewById(R.id.popup_layout);
+ final int displayHeight =
+ mService.getResources().getDisplayMetrics().heightPixels;
+ final int currentHeight = popupLayout.getLayoutParams().height;
+ final int keyboardHeight = keyboardView.getHeight();
+ if (mMinimumVoiceRecognitionViewHeightPixel > keyboardHeight
+ || mMinimumVoiceRecognitionViewHeightPixel > currentHeight) {
+ popupLayout.getLayoutParams().height =
+ mMinimumVoiceRecognitionViewHeightPixel;
+ } else if (keyboardHeight > currentHeight || keyboardHeight
+ > (displayHeight / RECOGNITIONVIEW_HEIGHT_THRESHOLD_RATIO)) {
+ popupLayout.getLayoutParams().height = keyboardHeight;
+ }
+ }
+ mService.setInputView(v);
+ mService.updateInputViewShown();
+
+ if (configuration != null) {
+ mVoiceInput.onConfigurationChanged(configuration);
+ }
+ }});
+ }
+
+ private void switchToLastInputMethod() {
+ final IBinder token = mService.getWindow().getWindow().getAttributes().token;
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return mImm.switchToLastInputMethod(token);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ // Calls in this method need to be done in the same thread as the thread which
+ // called switchToLastInputMethod()
+ if (!result) {
+ if (DEBUG) {
+ Log.d(TAG, "Couldn't switch back to last IME.");
+ }
+ // Because the current IME and subtype failed to switch to any other IME and
+ // subtype by switchToLastInputMethod, the current IME and subtype should keep
+ // being LatinIME and voice subtype in the next time. And for re-showing voice
+ // mode, the state of voice input should be reset and the voice view should be
+ // hidden.
+ mVoiceInput.reset();
+ mService.requestHideSelf(0);
+ } else {
+ // Notify an event that the current subtype was changed. This event will be
+ // handled if "onCurrentInputMethodSubtypeChanged" can't be implemented
+ // when the API level is 10 or previous.
+ mService.notifyOnCurrentInputMethodSubtypeChanged(null);
+ }
+ }
+ }.execute();
+ }
+
+ private void reallyStartListening(boolean swipe) {
+ if (!VOICE_INSTALLED) {
+ return;
+ }
+ if (!mHasUsedVoiceInput) {
+ // The user has started a voice input, so remember that in the
+ // future (so we don't show the warning dialog after the first run).
+ SharedPreferences.Editor editor =
+ PreferenceManager.getDefaultSharedPreferences(mService).edit();
+ editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true);
+ SharedPreferencesCompat.apply(editor);
+ mHasUsedVoiceInput = true;
+ }
+
+ if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) {
+ // The user has started a voice input from an unsupported locale, so remember that
+ // in the future (so we don't show the warning dialog the next time they do this).
+ SharedPreferences.Editor editor =
+ PreferenceManager.getDefaultSharedPreferences(mService).edit();
+ editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true);
+ SharedPreferencesCompat.apply(editor);
+ mHasUsedVoiceInputUnsupportedLocale = true;
+ }
+
+ // Clear N-best suggestions
+ mService.clearSuggestions();
+
+ FieldContext context = makeFieldContext();
+ mVoiceInput.startListening(context, swipe);
+ switchToRecognitionStatusView(null);
+ }
+
+ public void startListening(final boolean swipe, IBinder token) {
+ // TODO: remove swipe which is no longer used.
+ if (VOICE_INSTALLED) {
+ if (needsToShowWarningDialog()) {
+ // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel.
+ showVoiceWarningDialog(swipe, token);
+ } else {
+ reallyStartListening(swipe);
+ }
+ }
+ }
+
+ private boolean fieldCanDoVoice(FieldContext fieldContext) {
+ return !mPasswordText
+ && mVoiceInput != null
+ && !mVoiceInput.isBlacklistedField(fieldContext);
+ }
+
+ private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) {
+ @SuppressWarnings("deprecation")
+ final boolean noMic = Utils.inPrivateImeOptions(null,
+ LatinIME.IME_OPTION_NO_MICROPHONE_COMPAT, attribute)
+ || Utils.inPrivateImeOptions(mService.getPackageName(),
+ LatinIME.IME_OPTION_NO_MICROPHONE, attribute);
+ return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext) && !noMic
+ && SpeechRecognizer.isRecognitionAvailable(mService);
+ }
+
+ public void loadSettings(EditorInfo attribute, SharedPreferences sp) {
+ mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false);
+ mHasUsedVoiceInputUnsupportedLocale =
+ sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false);
+
+ mLocaleSupportedForVoiceInput = SubtypeSwitcher.getInstance().isVoiceSupported(
+ SubtypeSwitcher.getInstance().getInputLocaleStr());
+
+ if (VOICE_INSTALLED) {
+ final String voiceMode = sp.getString(PREF_VOICE_MODE,
+ mService.getString(R.string.voice_mode_main));
+ mVoiceButtonEnabled = !voiceMode.equals(mService.getString(R.string.voice_mode_off))
+ && shouldShowVoiceButton(makeFieldContext(), attribute);
+ mVoiceButtonOnPrimary = voiceMode.equals(mService.getString(R.string.voice_mode_main));
+ }
+ }
+
+ public void destroy() {
+ if (VOICE_INSTALLED && mVoiceInput != null) {
+ mVoiceInput.destroy();
+ }
+ }
+
+ public void onStartInputView(IBinder keyboardViewToken) {
+ // If keyboardViewToken is null, keyboardView is not attached but voiceView is attached.
+ IBinder windowToken = keyboardViewToken != null ? keyboardViewToken
+ : mVoiceInput.getView().getWindowToken();
+ // If IME is in voice mode, but still needs to show the voice warning dialog,
+ // keep showing the warning.
+ if (mSubtypeSwitcher.isVoiceMode() && windowToken != null) {
+ // Close keyboard view if it is been shown.
+ if (KeyboardSwitcher.getInstance().isInputViewShown())
+ KeyboardSwitcher.getInstance().getKeyboardView().purgeKeyboardAndClosing();
+ startListening(false, windowToken);
+ }
+ // If we have no token, onAttachedToWindow will take care of showing dialog and start
+ // listening.
+ }
+
+ public void onAttachedToWindow() {
+ // After onAttachedToWindow, we can show the voice warning dialog. See startListening()
+ // above.
+ VoiceInputWrapper.getInstance().setVoiceInput(mVoiceInput, mSubtypeSwitcher);
+ }
+
+ public void onConfigurationChanged(Configuration configuration) {
+ if (mRecognizing) {
+ switchToRecognitionStatusView(configuration);
+ }
+ }
+
+ @Override
+ public void onCancelVoice() {
+ if (mRecognizing) {
+ if (mSubtypeSwitcher.isVoiceMode()) {
+ // If voice mode is being canceled within LatinIME (i.e. time-out or user
+ // cancellation etc.), onCancelVoice() will be called first. LatinIME thinks it's
+ // still in voice mode. LatinIME needs to call switchToLastInputMethod().
+ // Note that onCancelVoice() will be called again from SubtypeSwitcher.
+ switchToLastInputMethod();
+ } else if (mSubtypeSwitcher.isKeyboardMode()) {
+ // If voice mode is being canceled out of LatinIME (i.e. by user's IME switching or
+ // as a result of switchToLastInputMethod() etc.),
+ // onCurrentInputMethodSubtypeChanged() will be called first. LatinIME will know
+ // that it's in keyboard mode and SubtypeSwitcher will call onCancelVoice().
+ mRecognizing = false;
+ mService.switchToKeyboardView();
+ }
+ }
+ }
+
+ @Override
+ public void onVoiceResults(List<String> candidates,
+ Map<String, List<CharSequence>> alternatives) {
+ if (!mRecognizing) {
+ return;
+ }
+ mVoiceResults.candidates = candidates;
+ mVoiceResults.alternatives = alternatives;
+ mHandler.updateVoiceResults();
+ }
+
+ private FieldContext makeFieldContext() {
+ SubtypeSwitcher switcher = SubtypeSwitcher.getInstance();
+ return new FieldContext(mService.getCurrentInputConnection(),
+ mService.getCurrentInputEditorInfo(), switcher.getInputLocaleStr(),
+ switcher.getEnabledLanguages());
+ }
+
+ private class VoiceResults {
+ List<String> candidates;
+ Map<String, List<CharSequence>> alternatives;
+ }
+
+ public static class VoiceLoggerWrapper {
+ private static final VoiceLoggerWrapper sLoggerWrapperInstance = new VoiceLoggerWrapper();
+ private VoiceInputLogger mLogger;
+
+ public static VoiceLoggerWrapper getInstance(Context context) {
+ if (sLoggerWrapperInstance.mLogger == null) {
+ // Not thread safe, but it's ok.
+ sLoggerWrapperInstance.mLogger = VoiceInputLogger.getLogger(context);
+ }
+ return sLoggerWrapperInstance;
+ }
+
+ // private for the singleton
+ private VoiceLoggerWrapper() {
+ }
+
+ public void settingsWarningDialogCancel() {
+ mLogger.settingsWarningDialogCancel();
+ }
+
+ public void settingsWarningDialogOk() {
+ mLogger.settingsWarningDialogOk();
+ }
+
+ public void settingsWarningDialogShown() {
+ mLogger.settingsWarningDialogShown();
+ }
+
+ public void settingsWarningDialogDismissed() {
+ mLogger.settingsWarningDialogDismissed();
+ }
+
+ public void voiceInputSettingEnabled(boolean enabled) {
+ if (enabled) {
+ mLogger.voiceInputSettingEnabled();
+ } else {
+ mLogger.voiceInputSettingDisabled();
+ }
+ }
+ }
+
+ public static class VoiceInputWrapper {
+ private static final VoiceInputWrapper sInputWrapperInstance = new VoiceInputWrapper();
+ private VoiceInput mVoiceInput;
+ public static VoiceInputWrapper getInstance() {
+ return sInputWrapperInstance;
+ }
+ public void setVoiceInput(VoiceInput voiceInput, SubtypeSwitcher switcher) {
+ if (mVoiceInput == null && voiceInput != null) {
+ mVoiceInput = voiceInput;
+ }
+ switcher.setVoiceInputWrapper(this);
+ }
+
+ private VoiceInputWrapper() {
+ }
+
+ public void cancel() {
+ if (mVoiceInput != null) mVoiceInput.cancel();
+ }
+
+ public void reset() {
+ if (mVoiceInput != null) mVoiceInput.reset();
+ }
+ }
+
+ // A list of locales which are supported by default for voice input, unless we get a
+ // different list from Gservices.
+ private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES =
+ "en " +
+ "en_US " +
+ "en_GB " +
+ "en_AU " +
+ "en_CA " +
+ "en_IE " +
+ "en_IN " +
+ "en_NZ " +
+ "en_SG " +
+ "en_ZA ";
+
+ public static String getSupportedLocalesString (ContentResolver resolver) {
+ return SettingsUtil.getSettingsString(
+ resolver,
+ SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES,
+ DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES);
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/compat/VoiceInputLoggerCompatUtils.java b/java/src/com/android/inputmethod/deprecated/compat/VoiceInputLoggerCompatUtils.java
new file mode 100644
index 000000000..488390fbc
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/compat/VoiceInputLoggerCompatUtils.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.deprecated.compat;
+
+import com.android.common.userhappiness.UserHappinessSignals;
+import com.android.inputmethod.compat.CompatUtils;
+
+import java.lang.reflect.Method;
+
+public class VoiceInputLoggerCompatUtils {
+ public static final String EXTRA_TEXT_REPLACED_LENGTH = "length";
+ public static final String EXTRA_BEFORE_N_BEST_CHOOSE = "before";
+ public static final String EXTRA_AFTER_N_BEST_CHOOSE = "after";
+ private static final Method METHOD_UserHappinessSignals_setHasVoiceLoggingInfo =
+ CompatUtils.getMethod(UserHappinessSignals.class, "setHasVoiceLoggingInfo",
+ boolean.class);
+
+ public static void setHasVoiceLoggingInfoCompat(boolean hasLoggingInfo) {
+ CompatUtils.invoke(null, null, METHOD_UserHappinessSignals_setHasVoiceLoggingInfo,
+ hasLoggingInfo);
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java b/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java
new file mode 100644
index 000000000..cf6cd0f5e
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2008-2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.languageswitcher;
+
+import com.android.inputmethod.keyboard.internal.KeyboardParser;
+import com.android.inputmethod.latin.DictionaryFactory;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.Settings;
+import com.android.inputmethod.latin.SharedPreferencesCompat;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.Utils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+public class InputLanguageSelection extends PreferenceActivity {
+
+ private SharedPreferences mPrefs;
+ private String mSelectedLanguages;
+ private HashMap<CheckBoxPreference, Locale> mLocaleMap =
+ new HashMap<CheckBoxPreference, Locale>();
+
+ private static class LocaleEntry implements Comparable<Object> {
+ private static Collator sCollator = Collator.getInstance();
+
+ private String mLabel;
+ public final Locale mLocale;
+
+ public LocaleEntry(String label, Locale locale) {
+ this.mLabel = label;
+ this.mLocale = locale;
+ }
+
+ @Override
+ public String toString() {
+ return this.mLabel;
+ }
+
+ @Override
+ public int compareTo(Object o) {
+ return sCollator.compare(this.mLabel, ((LocaleEntry) o).mLabel);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.language_prefs);
+ // Get the settings preferences
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mSelectedLanguages = mPrefs.getString(Settings.PREF_SELECTED_LANGUAGES, "");
+ String[] languageList = mSelectedLanguages.split(",");
+ ArrayList<LocaleEntry> availableLanguages = getUniqueLocales();
+ PreferenceGroup parent = getPreferenceScreen();
+ final HashMap<Long, LocaleEntry> dictionaryIdLocaleMap = new HashMap<Long, LocaleEntry>();
+ final TreeMap<LocaleEntry, Boolean> localeHasDictionaryMap =
+ new TreeMap<LocaleEntry, Boolean>();
+ for (int i = 0; i < availableLanguages.size(); i++) {
+ LocaleEntry loc = availableLanguages.get(i);
+ Locale locale = loc.mLocale;
+ final Pair<Long, Boolean> hasDictionaryOrLayout = hasDictionaryOrLayout(locale);
+ final Long dictionaryId = hasDictionaryOrLayout.first;
+ final boolean hasLayout = hasDictionaryOrLayout.second;
+ final boolean hasDictionary = dictionaryId != null;
+ // Add this locale to the supported list if:
+ // 1) this locale has a layout/ 2) this locale has a dictionary
+ // If some locales have no layout but have a same dictionary, the shortest locale
+ // will be added to the supported list.
+ if (!hasLayout && !hasDictionary) {
+ continue;
+ }
+ if (hasLayout) {
+ localeHasDictionaryMap.put(loc, hasDictionary);
+ }
+ if (!hasDictionary) {
+ continue;
+ }
+ if (dictionaryIdLocaleMap.containsKey(dictionaryId)) {
+ final String newLocale = locale.toString();
+ final String oldLocale =
+ dictionaryIdLocaleMap.get(dictionaryId).mLocale.toString();
+ // Check if this locale is more appropriate to be the candidate of the input locale.
+ if (oldLocale.length() <= newLocale.length() && !hasLayout) {
+ // Don't add this new locale to the map<dictionary id, locale> if:
+ // 1) the new locale's name is longer than the existing one, and
+ // 2) the new locale doesn't have its layout
+ continue;
+ }
+ }
+ dictionaryIdLocaleMap.put(dictionaryId, loc);
+ }
+
+ for (LocaleEntry localeEntry : dictionaryIdLocaleMap.values()) {
+ if (!localeHasDictionaryMap.containsKey(localeEntry)) {
+ localeHasDictionaryMap.put(localeEntry, true);
+ }
+ }
+
+ for (Entry<LocaleEntry, Boolean> entry : localeHasDictionaryMap.entrySet()) {
+ final LocaleEntry localeEntry = entry.getKey();
+ final Locale locale = localeEntry.mLocale;
+ final Boolean hasDictionary = entry.getValue();
+ CheckBoxPreference pref = new CheckBoxPreference(this);
+ pref.setTitle(localeEntry.mLabel);
+ boolean checked = isLocaleIn(locale, languageList);
+ pref.setChecked(checked);
+ if (hasDictionary) {
+ pref.setSummary(R.string.has_dictionary);
+ }
+ mLocaleMap.put(pref, locale);
+ parent.addPreference(pref);
+ }
+ }
+
+ private boolean isLocaleIn(Locale locale, String[] list) {
+ String lang = get5Code(locale);
+ for (int i = 0; i < list.length; i++) {
+ if (lang.equalsIgnoreCase(list[i])) return true;
+ }
+ return false;
+ }
+
+ private Pair<Long, Boolean> hasDictionaryOrLayout(Locale locale) {
+ if (locale == null) return new Pair<Long, Boolean>(null, false);
+ final Resources res = getResources();
+ final Locale saveLocale = Utils.setSystemLocale(res, locale);
+ final Long dictionaryId = DictionaryFactory.getDictionaryId(this, locale);
+ boolean hasLayout = false;
+
+ try {
+ final String localeStr = locale.toString();
+ final String[] layoutCountryCodes = KeyboardParser.parseKeyboardLocale(
+ this, R.xml.kbd_qwerty).split(",", -1);
+ if (!TextUtils.isEmpty(localeStr) && layoutCountryCodes.length > 0) {
+ for (String s : layoutCountryCodes) {
+ if (s.equals(localeStr)) {
+ hasLayout = true;
+ break;
+ }
+ }
+ }
+ } catch (XmlPullParserException e) {
+ } catch (IOException e) {
+ }
+ Utils.setSystemLocale(res, saveLocale);
+ return new Pair<Long, Boolean>(dictionaryId, hasLayout);
+ }
+
+ private String get5Code(Locale locale) {
+ String country = locale.getCountry();
+ return locale.getLanguage()
+ + (TextUtils.isEmpty(country) ? "" : "_" + country);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // Save the selected languages
+ String checkedLanguages = "";
+ PreferenceGroup parent = getPreferenceScreen();
+ int count = parent.getPreferenceCount();
+ for (int i = 0; i < count; i++) {
+ CheckBoxPreference pref = (CheckBoxPreference) parent.getPreference(i);
+ if (pref.isChecked()) {
+ checkedLanguages += get5Code(mLocaleMap.get(pref)) + ",";
+ }
+ }
+ if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null
+ Editor editor = mPrefs.edit();
+ editor.putString(Settings.PREF_SELECTED_LANGUAGES, checkedLanguages);
+ SharedPreferencesCompat.apply(editor);
+ }
+
+ public ArrayList<LocaleEntry> getUniqueLocales() {
+ String[] locales = getAssets().getLocales();
+ Arrays.sort(locales);
+ ArrayList<LocaleEntry> uniqueLocales = new ArrayList<LocaleEntry>();
+
+ final int origSize = locales.length;
+ LocaleEntry[] preprocess = new LocaleEntry[origSize];
+ int finalSize = 0;
+ for (int i = 0 ; i < origSize; i++ ) {
+ String s = locales[i];
+ int len = s.length();
+ String language = "";
+ String country = "";
+ if (len == 5) {
+ language = s.substring(0, 2);
+ country = s.substring(3, 5);
+ } else if (len < 5) {
+ language = s;
+ }
+ Locale l = new Locale(language, country);
+
+ // Exclude languages that are not relevant to LatinIME
+ if (TextUtils.isEmpty(language)) {
+ continue;
+ }
+
+ if (finalSize == 0) {
+ preprocess[finalSize++] =
+ new LocaleEntry(SubtypeSwitcher.getFullDisplayName(l, false), l);
+ } else {
+ if (s.equals("zz_ZZ")) {
+ // ignore this locale
+ } else {
+ final String displayName = SubtypeSwitcher.getFullDisplayName(l, false);
+ preprocess[finalSize++] = new LocaleEntry(displayName, l);
+ }
+ }
+ }
+ for (int i = 0; i < finalSize ; i++) {
+ uniqueLocales.add(preprocess[i]);
+ }
+ return uniqueLocales;
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java b/java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java
new file mode 100644
index 000000000..1eedb5ee1
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.languageswitcher;
+
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.Settings;
+import com.android.inputmethod.latin.SharedPreferencesCompat;
+import com.android.inputmethod.latin.Utils;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Configuration;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * Keeps track of list of selected input languages and the current
+ * input language that the user has selected.
+ */
+public class LanguageSwitcher {
+ private static final String TAG = LanguageSwitcher.class.getSimpleName();
+
+ @SuppressWarnings("unused")
+ private static final String KEYBOARD_MODE = "keyboard";
+ private static final String[] EMPTY_STIRNG_ARRAY = new String[0];
+
+ private final ArrayList<Locale> mLocales = new ArrayList<Locale>();
+ private final LatinIME mIme;
+ private String[] mSelectedLanguageArray = EMPTY_STIRNG_ARRAY;
+ private String mSelectedLanguages;
+ private int mCurrentIndex = 0;
+ private String mDefaultInputLanguage;
+ private Locale mDefaultInputLocale;
+ private Locale mSystemLocale;
+
+ public LanguageSwitcher(LatinIME ime) {
+ mIme = ime;
+ }
+
+ public int getLocaleCount() {
+ return mLocales.size();
+ }
+
+ public void onConfigurationChanged(Configuration conf, SharedPreferences prefs) {
+ final Locale newLocale = conf.locale;
+ if (!getSystemLocale().toString().equals(newLocale.toString())) {
+ loadLocales(prefs, newLocale);
+ }
+ }
+
+ /**
+ * Loads the currently selected input languages from shared preferences.
+ * @param sp shared preference for getting the current input language and enabled languages
+ * @param systemLocale the current system locale, stored for changing the current input language
+ * based on the system current system locale.
+ * @return whether there was any change
+ */
+ public boolean loadLocales(SharedPreferences sp, Locale systemLocale) {
+ if (LatinImeLogger.sDBG) {
+ Log.d(TAG, "load locales");
+ }
+ if (systemLocale != null) {
+ setSystemLocale(systemLocale);
+ }
+ String selectedLanguages = sp.getString(Settings.PREF_SELECTED_LANGUAGES, null);
+ String currentLanguage = sp.getString(Settings.PREF_INPUT_LANGUAGE, null);
+ if (TextUtils.isEmpty(selectedLanguages)) {
+ mSelectedLanguageArray = EMPTY_STIRNG_ARRAY;
+ mSelectedLanguages = null;
+ loadDefaults();
+ if (mLocales.size() == 0) {
+ return false;
+ }
+ mLocales.clear();
+ return true;
+ }
+ if (selectedLanguages.equals(mSelectedLanguages)) {
+ return false;
+ }
+ mSelectedLanguageArray = selectedLanguages.split(",");
+ mSelectedLanguages = selectedLanguages; // Cache it for comparison later
+ constructLocales();
+ mCurrentIndex = 0;
+ if (currentLanguage != null) {
+ // Find the index
+ mCurrentIndex = 0;
+ for (int i = 0; i < mLocales.size(); i++) {
+ if (mSelectedLanguageArray[i].equals(currentLanguage)) {
+ mCurrentIndex = i;
+ break;
+ }
+ }
+ // If we didn't find the index, use the first one
+ }
+ return true;
+ }
+
+ private void loadDefaults() {
+ if (LatinImeLogger.sDBG) {
+ Log.d(TAG, "load default locales:");
+ }
+ mDefaultInputLocale = mIme.getResources().getConfiguration().locale;
+ String country = mDefaultInputLocale.getCountry();
+ mDefaultInputLanguage = mDefaultInputLocale.getLanguage() +
+ (TextUtils.isEmpty(country) ? "" : "_" + country);
+ }
+
+ private void constructLocales() {
+ mLocales.clear();
+ for (final String lang : mSelectedLanguageArray) {
+ final Locale locale = Utils.constructLocaleFromString(lang);
+ mLocales.add(locale);
+ }
+ }
+
+ /**
+ * Returns the currently selected input language code, or the display language code if
+ * no specific locale was selected for input.
+ */
+ public String getInputLanguage() {
+ if (getLocaleCount() == 0) return mDefaultInputLanguage;
+
+ return mSelectedLanguageArray[mCurrentIndex];
+ }
+
+ /**
+ * Returns the list of enabled language codes.
+ */
+ public String[] getEnabledLanguages(boolean allowImplicitlySelectedLanguages) {
+ if (mSelectedLanguageArray.length == 0 && allowImplicitlySelectedLanguages) {
+ return new String[] { mDefaultInputLanguage };
+ }
+ return mSelectedLanguageArray;
+ }
+
+ /**
+ * Returns the currently selected input locale, or the display locale if no specific
+ * locale was selected for input.
+ */
+ public Locale getInputLocale() {
+ if (getLocaleCount() == 0) return mDefaultInputLocale;
+
+ return mLocales.get(mCurrentIndex);
+ }
+
+ private int nextLocaleIndex() {
+ final int size = mLocales.size();
+ return (mCurrentIndex + 1) % size;
+ }
+
+ private int prevLocaleIndex() {
+ final int size = mLocales.size();
+ return (mCurrentIndex - 1 + size) % size;
+ }
+
+ /**
+ * Returns the next input locale in the list. Wraps around to the beginning of the
+ * list if we're at the end of the list.
+ */
+ public Locale getNextInputLocale() {
+ if (getLocaleCount() == 0) return mDefaultInputLocale;
+ return mLocales.get(nextLocaleIndex());
+ }
+
+ /**
+ * Sets the system locale (display UI) used for comparing with the input language.
+ * @param locale the locale of the system
+ */
+ private void setSystemLocale(Locale locale) {
+ mSystemLocale = locale;
+ }
+
+ /**
+ * Returns the system locale.
+ * @return the system locale
+ */
+ private Locale getSystemLocale() {
+ return mSystemLocale;
+ }
+
+ /**
+ * Returns the previous input locale in the list. Wraps around to the end of the
+ * list if we're at the beginning of the list.
+ */
+ public Locale getPrevInputLocale() {
+ if (getLocaleCount() == 0) return mDefaultInputLocale;
+ return mLocales.get(prevLocaleIndex());
+ }
+
+ public void reset() {
+ mCurrentIndex = 0;
+ }
+
+ public void next() {
+ mCurrentIndex = nextLocaleIndex();
+ }
+
+ public void prev() {
+ mCurrentIndex = prevLocaleIndex();
+ }
+
+ public void setLocale(String localeStr) {
+ final int N = mLocales.size();
+ for (int i = 0; i < N; ++i) {
+ if (mLocales.get(i).toString().equals(localeStr)) {
+ mCurrentIndex = i;
+ }
+ }
+ }
+
+ public void persist(SharedPreferences prefs) {
+ Editor editor = prefs.edit();
+ editor.putString(Settings.PREF_INPUT_LANGUAGE, getInputLanguage());
+ SharedPreferencesCompat.apply(editor);
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/recorrection/Recorrection.java b/java/src/com/android/inputmethod/deprecated/recorrection/Recorrection.java
new file mode 100644
index 000000000..d40728d25
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/recorrection/Recorrection.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.recorrection;
+
+import com.android.inputmethod.compat.InputConnectionCompatUtils;
+import com.android.inputmethod.compat.SuggestionSpanUtils;
+import com.android.inputmethod.deprecated.VoiceProxy;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.latin.AutoCorrection;
+import com.android.inputmethod.latin.CandidateView;
+import com.android.inputmethod.latin.EditingUtils;
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.Settings;
+import com.android.inputmethod.latin.Suggest;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.TextEntryState;
+import com.android.inputmethod.latin.WordComposer;
+
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+import java.util.ArrayList;
+
+/**
+ * Manager of re-correction functionalities
+ */
+public class Recorrection implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final Recorrection sInstance = new Recorrection();
+
+ private LatinIME mService;
+ private boolean mRecorrectionEnabled = false;
+ private final ArrayList<RecorrectionSuggestionEntries> mRecorrectionSuggestionsList =
+ new ArrayList<RecorrectionSuggestionEntries>();
+
+ public static Recorrection getInstance() {
+ return sInstance;
+ }
+
+ public static void init(LatinIME context, SharedPreferences prefs) {
+ if (context == null || prefs == null) {
+ return;
+ }
+ sInstance.initInternal(context, prefs);
+ }
+
+ private Recorrection() {
+ }
+
+ public boolean isRecorrectionEnabled() {
+ return mRecorrectionEnabled;
+ }
+
+ private void initInternal(LatinIME context, SharedPreferences prefs) {
+ if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED) {
+ mRecorrectionEnabled = false;
+ return;
+ }
+ updateRecorrectionEnabled(context.getResources(), prefs);
+ mService = context;
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ public void checkRecorrectionOnStart() {
+ if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return;
+
+ final InputConnection ic = mService.getCurrentInputConnection();
+ if (ic == null) return;
+ // There could be a pending composing span. Clean it up first.
+ ic.finishComposingText();
+
+ if (mService.isShowingSuggestionsStrip() && mService.isSuggestionsRequested()) {
+ // First get the cursor position. This is required by setOldSuggestions(), so that
+ // it can pass the correct range to setComposingRegion(). At this point, we don't
+ // have valid values for mLastSelectionStart/End because onUpdateSelection() has
+ // not been called yet.
+ ExtractedTextRequest etr = new ExtractedTextRequest();
+ etr.token = 0; // anything is fine here
+ ExtractedText et = ic.getExtractedText(etr, 0);
+ if (et == null) return;
+ mService.setLastSelection(
+ et.startOffset + et.selectionStart, et.startOffset + et.selectionEnd);
+
+ // Then look for possible corrections in a delayed fashion
+ if (!TextUtils.isEmpty(et.text) && mService.isCursorTouchingWord()) {
+ mService.mHandler.postUpdateOldSuggestions();
+ }
+ }
+ }
+
+ public void updateRecorrectionSelection(KeyboardSwitcher keyboardSwitcher,
+ CandidateView candidateView, int candidatesStart, int candidatesEnd,
+ int newSelStart, int newSelEnd, int oldSelStart, int lastSelectionStart,
+ int lastSelectionEnd, boolean hasUncommittedTypedChars) {
+ if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return;
+ if (!mService.isShowingSuggestionsStrip()) return;
+ if (!keyboardSwitcher.isInputViewShown()) return;
+ if (!mService.isSuggestionsRequested()) return;
+ // Don't look for corrections if the keyboard is not visible
+ // Check if we should go in or out of correction mode.
+ if ((candidatesStart == candidatesEnd || newSelStart != oldSelStart || TextEntryState
+ .isRecorrecting())
+ && (newSelStart < newSelEnd - 1 || !hasUncommittedTypedChars)) {
+ if (mService.isCursorTouchingWord() || lastSelectionStart < lastSelectionEnd) {
+ mService.mHandler.cancelUpdateBigramPredictions();
+ mService.mHandler.postUpdateOldSuggestions();
+ } else {
+ abortRecorrection(false);
+ // If showing the "touch again to save" hint, do not replace it. Else,
+ // show the bigrams if we are at the end of the text, punctuation
+ // otherwise.
+ if (candidateView != null && !candidateView.isShowingAddToDictionaryHint()) {
+ InputConnection ic = mService.getCurrentInputConnection();
+ if (null == ic || !TextUtils.isEmpty(ic.getTextAfterCursor(1, 0))) {
+ if (!mService.isShowingPunctuationList()) {
+ mService.setPunctuationSuggestions();
+ }
+ } else {
+ mService.mHandler.postUpdateBigramPredictions();
+ }
+ }
+ }
+ }
+ }
+
+ public void saveRecorrectionSuggestion(WordComposer word, CharSequence result) {
+ if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return;
+ if (word.size() <= 1) {
+ return;
+ }
+ // Skip if result is null. It happens in some edge case.
+ if (TextUtils.isEmpty(result)) {
+ return;
+ }
+
+ // Make a copy of the CharSequence, since it is/could be a mutable CharSequence
+ final String resultCopy = result.toString();
+ RecorrectionSuggestionEntries entry = new RecorrectionSuggestionEntries(
+ resultCopy, new WordComposer(word));
+ mRecorrectionSuggestionsList.add(entry);
+ }
+
+ public void clearWordsInHistory() {
+ mRecorrectionSuggestionsList.clear();
+ }
+
+ /**
+ * Tries to apply any typed alternatives for the word if we have any cached alternatives,
+ * otherwise tries to find new corrections and completions for the word.
+ * @param touching The word that the cursor is touching, with position information
+ * @return true if an alternative was found, false otherwise.
+ */
+ public boolean applyTypedAlternatives(WordComposer word, Suggest suggest,
+ KeyboardSwitcher keyboardSwitcher, EditingUtils.SelectedWord touching) {
+ if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return false;
+ // If we didn't find a match, search for result in typed word history
+ WordComposer foundWord = null;
+ RecorrectionSuggestionEntries alternatives = null;
+ // Search old suggestions to suggest re-corrected suggestions.
+ for (RecorrectionSuggestionEntries entry : mRecorrectionSuggestionsList) {
+ if (TextUtils.equals(entry.getChosenWord(), touching.mWord)) {
+ foundWord = entry.mWordComposer;
+ alternatives = entry;
+ break;
+ }
+ }
+ // If we didn't find a match, at least suggest corrections as re-corrected suggestions.
+ if (foundWord == null
+ && (AutoCorrection.isValidWord(suggest.getUnigramDictionaries(),
+ touching.mWord, true))) {
+ foundWord = new WordComposer();
+ for (int i = 0; i < touching.mWord.length(); i++) {
+ foundWord.add(touching.mWord.charAt(i),
+ new int[] { touching.mWord.charAt(i) }, WordComposer.NOT_A_COORDINATE,
+ WordComposer.NOT_A_COORDINATE);
+ }
+ foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.mWord.charAt(0)));
+ }
+ // Found a match, show suggestions
+ if (foundWord != null || alternatives != null) {
+ if (alternatives == null) {
+ alternatives = new RecorrectionSuggestionEntries(touching.mWord, foundWord);
+ }
+ showRecorrections(suggest, keyboardSwitcher, alternatives);
+ if (foundWord != null) {
+ word.init(foundWord);
+ } else {
+ word.reset();
+ }
+ return true;
+ }
+ return false;
+ }
+
+
+ private void showRecorrections(Suggest suggest, KeyboardSwitcher keyboardSwitcher,
+ RecorrectionSuggestionEntries entries) {
+ SuggestedWords.Builder builder = entries.getAlternatives(suggest, keyboardSwitcher);
+ builder.setTypedWordValid(false).setHasMinimalSuggestion(false);
+ mService.showSuggestions(builder.build(), entries.getOriginalWord());
+ }
+
+ public void fetchAndDisplayRecorrectionSuggestions(VoiceProxy voiceProxy,
+ CandidateView candidateView, Suggest suggest, KeyboardSwitcher keyboardSwitcher,
+ WordComposer word, boolean hasUncommittedTypedChars, int lastSelectionStart,
+ int lastSelectionEnd, String wordSeparators) {
+ if (!InputConnectionCompatUtils.RECORRECTION_SUPPORTED) return;
+ if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return;
+ voiceProxy.setShowingVoiceSuggestions(false);
+ if (candidateView != null && candidateView.isShowingAddToDictionaryHint()) {
+ return;
+ }
+ InputConnection ic = mService.getCurrentInputConnection();
+ if (ic == null) return;
+ if (!hasUncommittedTypedChars) {
+ // Extract the selected or touching text
+ EditingUtils.SelectedWord touching = EditingUtils.getWordAtCursorOrSelection(ic,
+ lastSelectionStart, lastSelectionEnd, wordSeparators);
+
+ if (touching != null && touching.mWord.length() > 1) {
+ ic.beginBatchEdit();
+
+ if (applyTypedAlternatives(word, suggest, keyboardSwitcher, touching)
+ || voiceProxy.applyVoiceAlternatives(touching)) {
+ TextEntryState.selectedForRecorrection();
+ InputConnectionCompatUtils.underlineWord(ic, touching);
+ } else {
+ abortRecorrection(true);
+ }
+
+ ic.endBatchEdit();
+ } else {
+ abortRecorrection(true);
+ mService.updateBigramPredictions();
+ }
+ } else {
+ abortRecorrection(true);
+ }
+ }
+
+ public void abortRecorrection(boolean force) {
+ if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED) return;
+ if (force || TextEntryState.isRecorrecting()) {
+ TextEntryState.onAbortRecorrection();
+ mService.setCandidatesViewShown(mService.isCandidateStripVisible());
+ mService.getCurrentInputConnection().finishComposingText();
+ mService.clearSuggestions();
+ }
+ }
+
+ public void updateRecorrectionEnabled(Resources res, SharedPreferences prefs) {
+ // If the option should not be shown, do not read the re-correction preference
+ // but always use the default setting defined in the resources.
+ if (res.getBoolean(R.bool.config_enable_show_recorrection_option)) {
+ mRecorrectionEnabled = prefs.getBoolean(Settings.PREF_RECORRECTION_ENABLED,
+ res.getBoolean(R.bool.config_default_recorrection_enabled));
+ } else {
+ mRecorrectionEnabled = res.getBoolean(R.bool.config_default_recorrection_enabled);
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED) return;
+ if (key.equals(Settings.PREF_RECORRECTION_ENABLED)) {
+ updateRecorrectionEnabled(mService.getResources(), prefs);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/recorrection/RecorrectionSuggestionEntries.java b/java/src/com/android/inputmethod/deprecated/recorrection/RecorrectionSuggestionEntries.java
new file mode 100644
index 000000000..5e6c87044
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/recorrection/RecorrectionSuggestionEntries.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.recorrection;
+
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.latin.Suggest;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.WordComposer;
+
+import android.text.TextUtils;
+
+public class RecorrectionSuggestionEntries {
+ public final CharSequence mChosenWord;
+ public final WordComposer mWordComposer;
+
+ public RecorrectionSuggestionEntries(CharSequence chosenWord, WordComposer wordComposer) {
+ mChosenWord = chosenWord;
+ mWordComposer = wordComposer;
+ }
+
+ public CharSequence getChosenWord() {
+ return mChosenWord;
+ }
+
+ public CharSequence getOriginalWord() {
+ return mWordComposer.getTypedWord();
+ }
+
+ public SuggestedWords.Builder getAlternatives(
+ Suggest suggest, KeyboardSwitcher keyboardSwitcher) {
+ return getTypedSuggestions(suggest, keyboardSwitcher, mWordComposer);
+ }
+
+ @Override
+ public int hashCode() {
+ return mChosenWord.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof CharSequence && TextUtils.equals(mChosenWord, (CharSequence)o);
+ }
+
+ private static SuggestedWords.Builder getTypedSuggestions(
+ Suggest suggest, KeyboardSwitcher keyboardSwitcher, WordComposer word) {
+ return suggest.getSuggestedWordBuilder(keyboardSwitcher.getKeyboardView(), word, null);
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/FieldContext.java b/java/src/com/android/inputmethod/deprecated/voice/FieldContext.java
new file mode 100644
index 000000000..3c79cc218
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/FieldContext.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Represents information about a given text field, which can be passed
+ * to the speech recognizer as context information.
+ */
+public class FieldContext {
+ private static final boolean DBG = false;
+
+ static final String LABEL = "label";
+ static final String HINT = "hint";
+ static final String PACKAGE_NAME = "packageName";
+ static final String FIELD_ID = "fieldId";
+ static final String FIELD_NAME = "fieldName";
+ static final String SINGLE_LINE = "singleLine";
+ static final String INPUT_TYPE = "inputType";
+ static final String IME_OPTIONS = "imeOptions";
+ static final String SELECTED_LANGUAGE = "selectedLanguage";
+ static final String ENABLED_LANGUAGES = "enabledLanguages";
+
+ Bundle mFieldInfo;
+
+ public FieldContext(InputConnection conn, EditorInfo info,
+ String selectedLanguage, String[] enabledLanguages) {
+ mFieldInfo = new Bundle();
+ addEditorInfoToBundle(info, mFieldInfo);
+ addInputConnectionToBundle(conn, mFieldInfo);
+ addLanguageInfoToBundle(selectedLanguage, enabledLanguages, mFieldInfo);
+ if (DBG) Log.i("FieldContext", "Bundle = " + mFieldInfo.toString());
+ }
+
+ private static String safeToString(Object o) {
+ if (o == null) {
+ return "";
+ }
+ return o.toString();
+ }
+
+ private static void addEditorInfoToBundle(EditorInfo info, Bundle bundle) {
+ if (info == null) {
+ return;
+ }
+
+ bundle.putString(LABEL, safeToString(info.label));
+ bundle.putString(HINT, safeToString(info.hintText));
+ bundle.putString(PACKAGE_NAME, safeToString(info.packageName));
+ bundle.putInt(FIELD_ID, info.fieldId);
+ bundle.putString(FIELD_NAME, safeToString(info.fieldName));
+ bundle.putInt(INPUT_TYPE, info.inputType);
+ bundle.putInt(IME_OPTIONS, info.imeOptions);
+ }
+
+ @SuppressWarnings("static-access")
+ private static void addInputConnectionToBundle(
+ InputConnection conn, Bundle bundle) {
+ if (conn == null) {
+ return;
+ }
+
+ ExtractedText et = conn.getExtractedText(new ExtractedTextRequest(), 0);
+ if (et == null) {
+ return;
+ }
+ bundle.putBoolean(SINGLE_LINE, (et.flags & et.FLAG_SINGLE_LINE) > 0);
+ }
+
+ private static void addLanguageInfoToBundle(
+ String selectedLanguage, String[] enabledLanguages, Bundle bundle) {
+ bundle.putString(SELECTED_LANGUAGE, selectedLanguage);
+ bundle.putStringArray(ENABLED_LANGUAGES, enabledLanguages);
+ }
+
+ public Bundle getBundle() {
+ return mFieldInfo;
+ }
+
+ @Override
+ public String toString() {
+ return mFieldInfo.toString();
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/Hints.java b/java/src/com/android/inputmethod/deprecated/voice/Hints.java
new file mode 100644
index 000000000..06b234381
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/Hints.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SharedPreferencesCompat;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.view.inputmethod.InputConnection;
+
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Logic to determine when to display hints on usage to the user.
+ */
+public class Hints {
+ public interface Display {
+ public void showHint(int viewResource);
+ }
+
+ private static final String PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN =
+ "voice_hint_num_unique_days_shown";
+ private static final String PREF_VOICE_HINT_LAST_TIME_SHOWN =
+ "voice_hint_last_time_shown";
+ private static final String PREF_VOICE_INPUT_LAST_TIME_USED =
+ "voice_input_last_time_used";
+ private static final String PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT =
+ "voice_punctuation_hint_view_count";
+ private static final int DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW = 7;
+ private static final int DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS = 7;
+
+ private final Context mContext;
+ private final SharedPreferences mPrefs;
+ private final Display mDisplay;
+ private boolean mVoiceResultContainedPunctuation;
+ private int mSwipeHintMaxDaysToShow;
+ private int mPunctuationHintMaxDisplays;
+
+ // Only show punctuation hint if voice result did not contain punctuation.
+ static final Map<CharSequence, String> SPEAKABLE_PUNCTUATION
+ = new HashMap<CharSequence, String>();
+ static {
+ SPEAKABLE_PUNCTUATION.put(",", "comma");
+ SPEAKABLE_PUNCTUATION.put(".", "period");
+ SPEAKABLE_PUNCTUATION.put("?", "question mark");
+ }
+
+ public Hints(Context context, SharedPreferences prefs, Display display) {
+ mContext = context;
+ mPrefs = prefs;
+ mDisplay = display;
+
+ ContentResolver cr = mContext.getContentResolver();
+ mSwipeHintMaxDaysToShow = SettingsUtil.getSettingsInt(
+ cr,
+ SettingsUtil.LATIN_IME_VOICE_INPUT_SWIPE_HINT_MAX_DAYS,
+ DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW);
+ mPunctuationHintMaxDisplays = SettingsUtil.getSettingsInt(
+ cr,
+ SettingsUtil.LATIN_IME_VOICE_INPUT_PUNCTUATION_HINT_MAX_DISPLAYS,
+ DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS);
+ }
+
+ public boolean showSwipeHintIfNecessary(boolean fieldRecommended) {
+ if (fieldRecommended && shouldShowSwipeHint()) {
+ showHint(R.layout.voice_swipe_hint);
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean showPunctuationHintIfNecessary(InputConnection ic) {
+ if (!mVoiceResultContainedPunctuation
+ && ic != null
+ && getAndIncrementPref(PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT)
+ < mPunctuationHintMaxDisplays) {
+ CharSequence charBeforeCursor = ic.getTextBeforeCursor(1, 0);
+ if (SPEAKABLE_PUNCTUATION.containsKey(charBeforeCursor)) {
+ showHint(R.layout.voice_punctuation_hint);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void registerVoiceResult(String text) {
+ // Update the current time as the last time voice input was used.
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putLong(PREF_VOICE_INPUT_LAST_TIME_USED, System.currentTimeMillis());
+ SharedPreferencesCompat.apply(editor);
+
+ mVoiceResultContainedPunctuation = false;
+ for (CharSequence s : SPEAKABLE_PUNCTUATION.keySet()) {
+ if (text.indexOf(s.toString()) >= 0) {
+ mVoiceResultContainedPunctuation = true;
+ break;
+ }
+ }
+ }
+
+ private boolean shouldShowSwipeHint() {
+ final SharedPreferences prefs = mPrefs;
+
+ int numUniqueDaysShown = prefs.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0);
+
+ // If we've already shown the hint for enough days, we'll return false.
+ if (numUniqueDaysShown < mSwipeHintMaxDaysToShow) {
+
+ long lastTimeVoiceWasUsed = prefs.getLong(PREF_VOICE_INPUT_LAST_TIME_USED, 0);
+
+ // If the user has used voice today, we'll return false. (We don't show the hint on
+ // any day that the user has already used voice.)
+ if (!isFromToday(lastTimeVoiceWasUsed)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines whether the provided time is from some time today (i.e., this day, month,
+ * and year).
+ */
+ private boolean isFromToday(long timeInMillis) {
+ if (timeInMillis == 0) return false;
+
+ Calendar today = Calendar.getInstance();
+ today.setTimeInMillis(System.currentTimeMillis());
+
+ Calendar timestamp = Calendar.getInstance();
+ timestamp.setTimeInMillis(timeInMillis);
+
+ return (today.get(Calendar.YEAR) == timestamp.get(Calendar.YEAR) &&
+ today.get(Calendar.DAY_OF_MONTH) == timestamp.get(Calendar.DAY_OF_MONTH) &&
+ today.get(Calendar.MONTH) == timestamp.get(Calendar.MONTH));
+ }
+
+ private void showHint(int hintViewResource) {
+ final SharedPreferences prefs = mPrefs;
+
+ int numUniqueDaysShown = prefs.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0);
+ long lastTimeHintWasShown = prefs.getLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, 0);
+
+ // If this is the first time the hint is being shown today, increase the saved values
+ // to represent that. We don't need to increase the last time the hint was shown unless
+ // it is a different day from the current value.
+ if (!isFromToday(lastTimeHintWasShown)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, numUniqueDaysShown + 1);
+ editor.putLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, System.currentTimeMillis());
+ SharedPreferencesCompat.apply(editor);
+ }
+
+ if (mDisplay != null) {
+ mDisplay.showHint(hintViewResource);
+ }
+ }
+
+ private int getAndIncrementPref(String pref) {
+ final SharedPreferences prefs = mPrefs;
+ int value = prefs.getInt(pref, 0);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(pref, value + 1);
+ SharedPreferencesCompat.apply(editor);
+ return value;
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java b/java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java
new file mode 100644
index 000000000..dcb826e8f
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathEffect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+import java.util.Locale;
+
+/**
+ * The user interface for the "Speak now" and "working" states.
+ * Displays a recognition dialog (with waveform, voice meter, etc.),
+ * plays beeps, shows errors, etc.
+ */
+public class RecognitionView {
+ private static final String TAG = "RecognitionView";
+
+ private Handler mUiHandler; // Reference to UI thread
+ private View mView;
+ private Context mContext;
+
+ private TextView mText;
+ private ImageView mImage;
+ private View mProgress;
+ private SoundIndicator mSoundIndicator;
+ private TextView mLanguage;
+ private Button mButton;
+
+ private Drawable mInitializing;
+ private Drawable mError;
+
+ private static final int INIT = 0;
+ private static final int LISTENING = 1;
+ private static final int WORKING = 2;
+ private static final int READY = 3;
+
+ private int mState = INIT;
+
+ private final View mPopupLayout;
+
+ private final Drawable mListeningBorder;
+ private final Drawable mWorkingBorder;
+ private final Drawable mErrorBorder;
+
+ public RecognitionView(Context context, OnClickListener clickListener) {
+ mUiHandler = new Handler();
+
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+
+ mView = inflater.inflate(R.layout.recognition_status, null);
+
+ mPopupLayout= mView.findViewById(R.id.popup_layout);
+
+ // Pre-load volume level images
+ Resources r = context.getResources();
+
+ mListeningBorder = r.getDrawable(R.drawable.vs_dialog_red);
+ mWorkingBorder = r.getDrawable(R.drawable.vs_dialog_blue);
+ mErrorBorder = r.getDrawable(R.drawable.vs_dialog_yellow);
+
+ mInitializing = r.getDrawable(R.drawable.mic_slash);
+ mError = r.getDrawable(R.drawable.caution);
+
+ mImage = (ImageView) mView.findViewById(R.id.image);
+ mProgress = mView.findViewById(R.id.progress);
+ mSoundIndicator = (SoundIndicator) mView.findViewById(R.id.sound_indicator);
+
+ mButton = (Button) mView.findViewById(R.id.button);
+ mButton.setOnClickListener(clickListener);
+ mText = (TextView) mView.findViewById(R.id.text);
+ mLanguage = (TextView) mView.findViewById(R.id.language);
+
+ mContext = context;
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ public void restoreState() {
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Restart the spinner
+ if (mState == WORKING) {
+ ((ProgressBar) mProgress).setIndeterminate(false);
+ ((ProgressBar) mProgress).setIndeterminate(true);
+ }
+ }
+ });
+ }
+
+ public void showInitializing() {
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mState = INIT;
+ prepareDialog(mContext.getText(R.string.voice_initializing), mInitializing,
+ mContext.getText(R.string.cancel));
+ }
+ });
+ }
+
+ public void showListening() {
+ Log.d(TAG, "#showListening");
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mState = LISTENING;
+ prepareDialog(mContext.getText(R.string.voice_listening), null,
+ mContext.getText(R.string.cancel));
+ }
+ });
+ }
+
+ public void updateVoiceMeter(float rmsdB) {
+ mSoundIndicator.setRmsdB(rmsdB);
+ }
+
+ public void showError(final String message) {
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mState = READY;
+ prepareDialog(message, mError, mContext.getText(R.string.ok));
+ }
+ });
+ }
+
+ public void showWorking(
+ final ByteArrayOutputStream waveBuffer,
+ final int speechStartPosition,
+ final int speechEndPosition) {
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mState = WORKING;
+ prepareDialog(mContext.getText(R.string.voice_working), null, mContext
+ .getText(R.string.cancel));
+ final ShortBuffer buf = ByteBuffer.wrap(waveBuffer.toByteArray()).order(
+ ByteOrder.nativeOrder()).asShortBuffer();
+ buf.position(0);
+ waveBuffer.reset();
+ showWave(buf, speechStartPosition / 2, speechEndPosition / 2);
+ }
+ });
+ }
+
+ private void prepareDialog(CharSequence text, Drawable image,
+ CharSequence btnTxt) {
+
+ /*
+ * The mic of INIT and of LISTENING has to be displayed in the same position. To accomplish
+ * that, some text visibility are not set as GONE but as INVISIBLE.
+ */
+ switch (mState) {
+ case INIT:
+ mText.setVisibility(View.INVISIBLE);
+
+ mProgress.setVisibility(View.GONE);
+
+ mImage.setVisibility(View.VISIBLE);
+ mImage.setImageResource(R.drawable.mic_slash);
+
+ mSoundIndicator.setVisibility(View.GONE);
+ mSoundIndicator.stop();
+
+ mLanguage.setVisibility(View.INVISIBLE);
+
+ mPopupLayout.setBackgroundDrawable(mListeningBorder);
+ break;
+ case LISTENING:
+ mText.setVisibility(View.VISIBLE);
+ mText.setText(text);
+
+ mProgress.setVisibility(View.GONE);
+
+ mImage.setVisibility(View.GONE);
+
+ mSoundIndicator.setVisibility(View.VISIBLE);
+ mSoundIndicator.start();
+
+ Locale locale = SubtypeSwitcher.getInstance().getInputLocale();
+
+ mLanguage.setVisibility(View.VISIBLE);
+ mLanguage.setText(SubtypeSwitcher.getFullDisplayName(locale, true));
+
+ mPopupLayout.setBackgroundDrawable(mListeningBorder);
+ break;
+ case WORKING:
+
+ mText.setVisibility(View.VISIBLE);
+ mText.setText(text);
+
+ mProgress.setVisibility(View.VISIBLE);
+
+ mImage.setVisibility(View.VISIBLE);
+
+ mSoundIndicator.setVisibility(View.GONE);
+ mSoundIndicator.stop();
+
+ mLanguage.setVisibility(View.GONE);
+
+ mPopupLayout.setBackgroundDrawable(mWorkingBorder);
+ break;
+ case READY:
+ mText.setVisibility(View.VISIBLE);
+ mText.setText(text);
+
+ mProgress.setVisibility(View.GONE);
+
+ mImage.setVisibility(View.VISIBLE);
+ mImage.setImageResource(R.drawable.caution);
+
+ mSoundIndicator.setVisibility(View.GONE);
+ mSoundIndicator.stop();
+
+ mLanguage.setVisibility(View.GONE);
+
+ mPopupLayout.setBackgroundDrawable(mErrorBorder);
+ break;
+ default:
+ Log.w(TAG, "Unknown state " + mState);
+ }
+ mPopupLayout.requestLayout();
+ mButton.setText(btnTxt);
+ }
+
+ /**
+ * @return an average abs of the specified buffer.
+ */
+ private static int getAverageAbs(ShortBuffer buffer, int start, int i, int npw) {
+ int from = start + i * npw;
+ int end = from + npw;
+ int total = 0;
+ for (int x = from; x < end; x++) {
+ total += Math.abs(buffer.get(x));
+ }
+ return total / npw;
+ }
+
+
+ /**
+ * Shows waveform of input audio.
+ *
+ * Copied from version in VoiceSearch's RecognitionActivity.
+ *
+ * TODO: adjust stroke width based on the size of data.
+ * TODO: use dip rather than pixels.
+ */
+ private void showWave(ShortBuffer waveBuffer, int startPosition, int endPosition) {
+ final int w = ((View) mImage.getParent()).getWidth();
+ final int h = ((View) mImage.getParent()).getHeight();
+ if (w <= 0 || h <= 0) {
+ // view is not visible this time. Skip drawing.
+ return;
+ }
+ final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ final Canvas c = new Canvas(b);
+ final Paint paint = new Paint();
+ paint.setColor(0xFFFFFFFF); // 0xAARRGGBB
+ paint.setAntiAlias(true);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setAlpha(80);
+
+ final PathEffect effect = new CornerPathEffect(3);
+ paint.setPathEffect(effect);
+
+ final int numSamples = waveBuffer.remaining();
+ int endIndex;
+ if (endPosition == 0) {
+ endIndex = numSamples;
+ } else {
+ endIndex = Math.min(endPosition, numSamples);
+ }
+
+ int startIndex = startPosition - 2000; // include 250ms before speech
+ if (startIndex < 0) {
+ startIndex = 0;
+ }
+ final int numSamplePerWave = 200; // 8KHz 25ms = 200 samples
+ final float scale = 10.0f / 65536.0f;
+
+ final int count = (endIndex - startIndex) / numSamplePerWave;
+ final float deltaX = 1.0f * w / count;
+ int yMax = h / 2;
+ Path path = new Path();
+ c.translate(0, yMax);
+ float x = 0;
+ path.moveTo(x, 0);
+ for (int i = 0; i < count; i++) {
+ final int avabs = getAverageAbs(waveBuffer, startIndex, i , numSamplePerWave);
+ int sign = ( (i & 01) == 0) ? -1 : 1;
+ final float y = Math.min(yMax, avabs * h * scale) * sign;
+ path.lineTo(x, y);
+ x += deltaX;
+ path.lineTo(x, y);
+ }
+ if (deltaX > 4) {
+ paint.setStrokeWidth(2);
+ } else {
+ paint.setStrokeWidth(Math.max(0, (int) (deltaX -.05)));
+ }
+ c.drawPath(path, paint);
+ mImage.setImageBitmap(b);
+ }
+
+ public void finish() {
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSoundIndicator.stop();
+ }
+ });
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java b/java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java
new file mode 100644
index 000000000..855a09a1d
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+
+/**
+ * Utility for retrieving settings from Settings.Secure.
+ */
+public class SettingsUtil {
+ /**
+ * A whitespace-separated list of supported locales for voice input from the keyboard.
+ */
+ public static final String LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES =
+ "latin_ime_voice_input_supported_locales";
+
+ /**
+ * A whitespace-separated list of recommended app packages for voice input from the
+ * keyboard.
+ */
+ public static final String LATIN_IME_VOICE_INPUT_RECOMMENDED_PACKAGES =
+ "latin_ime_voice_input_recommended_packages";
+
+ /**
+ * The maximum number of unique days to show the swipe hint for voice input.
+ */
+ public static final String LATIN_IME_VOICE_INPUT_SWIPE_HINT_MAX_DAYS =
+ "latin_ime_voice_input_swipe_hint_max_days";
+
+ /**
+ * The maximum number of times to show the punctuation hint for voice input.
+ */
+ public static final String LATIN_IME_VOICE_INPUT_PUNCTUATION_HINT_MAX_DISPLAYS =
+ "latin_ime_voice_input_punctuation_hint_max_displays";
+
+ /**
+ * Endpointer parameters for voice input from the keyboard.
+ */
+ public static final String LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS =
+ "latin_ime_speech_minimum_length_millis";
+ public static final String LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS =
+ "latin_ime_speech_input_complete_silence_length_millis";
+ public static final String LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS =
+ "latin_ime_speech_input_possibly_complete_silence_length_millis";
+
+ /**
+ * Min and max volume levels that can be displayed on the "speak now" screen.
+ */
+ public static final String LATIN_IME_MIN_MICROPHONE_LEVEL =
+ "latin_ime_min_microphone_level";
+ public static final String LATIN_IME_MAX_MICROPHONE_LEVEL =
+ "latin_ime_max_microphone_level";
+
+ /**
+ * The number of sentence-level alternates to request of the server.
+ */
+ public static final String LATIN_IME_MAX_VOICE_RESULTS = "latin_ime_max_voice_results";
+
+ /**
+ * Get a string-valued setting.
+ *
+ * @param cr The content resolver to use
+ * @param key The setting to look up
+ * @param defaultValue The default value to use if none can be found
+ * @return The value of the setting, or defaultValue if it couldn't be found
+ */
+ public static String getSettingsString(ContentResolver cr, String key, String defaultValue) {
+ String result = Settings.Secure.getString(cr, key);
+ return (result == null) ? defaultValue : result;
+ }
+
+ /**
+ * Get an int-valued setting.
+ *
+ * @param cr The content resolver to use
+ * @param key The setting to look up
+ * @param defaultValue The default value to use if the setting couldn't be found or parsed
+ * @return The value of the setting, or defaultValue if it couldn't be found or parsed
+ */
+ public static int getSettingsInt(ContentResolver cr, String key, int defaultValue) {
+ return Settings.Secure.getInt(cr, key, defaultValue);
+ }
+
+ /**
+ * Get a float-valued setting.
+ *
+ * @param cr The content resolver to use
+ * @param key The setting to look up
+ * @param defaultValue The default value to use if the setting couldn't be found or parsed
+ * @return The value of the setting, or defaultValue if it couldn't be found or parsed
+ */
+ public static float getSettingsFloat(ContentResolver cr, String key, float defaultValue) {
+ return Settings.Secure.getFloat(cr, key, defaultValue);
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java b/java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java
new file mode 100644
index 000000000..25b314085
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import com.android.inputmethod.latin.R;
+
+/**
+ * A widget which shows the volume of audio using a microphone icon
+ */
+public class SoundIndicator extends ImageView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SoundIndicator";
+
+ private static final float UP_SMOOTHING_FACTOR = 0.9f;
+ private static final float DOWN_SMOOTHING_FACTOR = 0.4f;
+
+ private static final float AUDIO_METER_MIN_DB = 7.0f;
+ private static final float AUDIO_METER_DB_RANGE = 20.0f;
+
+ private static final long FRAME_DELAY = 50;
+
+ private Bitmap mDrawingBuffer;
+ private Canvas mBufferCanvas;
+ private Bitmap mEdgeBitmap;
+ private float mLevel = 0.0f;
+ private Drawable mFrontDrawable;
+ private Paint mClearPaint;
+ private Paint mMultPaint;
+ private int mEdgeBitmapOffset;
+
+ private Handler mHandler;
+
+ private Runnable mDrawFrame = new Runnable() {
+ public void run() {
+ invalidate();
+ mHandler.postDelayed(mDrawFrame, FRAME_DELAY);
+ }
+ };
+
+ public SoundIndicator(Context context) {
+ this(context, null);
+ }
+
+ public SoundIndicator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mFrontDrawable = getDrawable();
+ BitmapDrawable edgeDrawable =
+ (BitmapDrawable) context.getResources().getDrawable(R.drawable.vs_popup_mic_edge);
+ mEdgeBitmap = edgeDrawable.getBitmap();
+ mEdgeBitmapOffset = mEdgeBitmap.getHeight() / 2;
+
+ mDrawingBuffer =
+ Bitmap.createBitmap(mFrontDrawable.getIntrinsicWidth(),
+ mFrontDrawable.getIntrinsicHeight(), Config.ARGB_8888);
+
+ mBufferCanvas = new Canvas(mDrawingBuffer);
+
+ // Initialize Paints.
+ mClearPaint = new Paint();
+ mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+
+ mMultPaint = new Paint();
+ mMultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
+
+ mHandler = new Handler();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ //super.onDraw(canvas);
+
+ float w = getWidth();
+ float h = getHeight();
+
+ // Clear the buffer canvas
+ mBufferCanvas.drawRect(0, 0, w, h, mClearPaint);
+
+ // Set its clip so we don't draw the front image all the way to the top
+ Rect clip = new Rect(0,
+ (int) ((1.0 - mLevel) * (h + mEdgeBitmapOffset)) - mEdgeBitmapOffset,
+ (int) w,
+ (int) h);
+
+ mBufferCanvas.save();
+ mBufferCanvas.clipRect(clip);
+
+ // Draw the front image
+ mFrontDrawable.setBounds(new Rect(0, 0, (int) w, (int) h));
+ mFrontDrawable.draw(mBufferCanvas);
+
+ mBufferCanvas.restore();
+
+ // Draw the edge image on top of the buffer image with a multiply mode
+ mBufferCanvas.drawBitmap(mEdgeBitmap, 0, clip.top, mMultPaint);
+
+ // Draw the buffer image (on top of the background image)
+ canvas.drawBitmap(mDrawingBuffer, 0, 0, null);
+ }
+
+ /**
+ * Sets the sound level
+ *
+ * @param rmsdB The level of the sound, in dB.
+ */
+ public void setRmsdB(float rmsdB) {
+ float level = ((rmsdB - AUDIO_METER_MIN_DB) / AUDIO_METER_DB_RANGE);
+
+ level = Math.min(Math.max(0.0f, level), 1.0f);
+
+ // We smooth towards the new level
+ if (level > mLevel) {
+ mLevel = (level - mLevel) * UP_SMOOTHING_FACTOR + mLevel;
+ } else {
+ mLevel = (level - mLevel) * DOWN_SMOOTHING_FACTOR + mLevel;
+ }
+ invalidate();
+ }
+
+ public void start() {
+ mHandler.post(mDrawFrame);
+ }
+
+ public void stop() {
+ mHandler.removeCallbacks(mDrawFrame);
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java b/java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java
new file mode 100644
index 000000000..b718ebbb7
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java
@@ -0,0 +1,685 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import com.android.inputmethod.latin.EditingUtils;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.speech.RecognitionListener;
+import android.speech.RecognizerIntent;
+import android.speech.SpeechRecognizer;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.inputmethod.InputConnection;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Speech recognition input, including both user interface and a background
+ * process to stream audio to the network recognizer. This class supplies a
+ * View (getView()), which it updates as recognition occurs. The user of this
+ * class is responsible for making the view visible to the user, as well as
+ * handling various events returned through UiListener.
+ */
+public class VoiceInput implements OnClickListener {
+ private static final String TAG = "VoiceInput";
+ private static final String EXTRA_RECOGNITION_CONTEXT =
+ "android.speech.extras.RECOGNITION_CONTEXT";
+ private static final String EXTRA_CALLING_PACKAGE = "calling_package";
+ private static final String EXTRA_ALTERNATES = "android.speech.extra.ALTERNATES";
+ private static final int MAX_ALT_LIST_LENGTH = 6;
+ private static boolean DBG = LatinImeLogger.sDBG;
+
+ private static final String DEFAULT_RECOMMENDED_PACKAGES =
+ "com.android.mms " +
+ "com.google.android.gm " +
+ "com.google.android.talk " +
+ "com.google.android.apps.googlevoice " +
+ "com.android.email " +
+ "com.android.browser ";
+
+ // WARNING! Before enabling this, fix the problem with calling getExtractedText() in
+ // landscape view. It causes Extracted text updates to be rejected due to a token mismatch
+ public static boolean ENABLE_WORD_CORRECTIONS = true;
+
+ // Dummy word suggestion which means "delete current word"
+ public static final String DELETE_SYMBOL = " \u00D7 "; // times symbol
+
+ private Whitelist mRecommendedList;
+ private Whitelist mBlacklist;
+
+ private VoiceInputLogger mLogger;
+
+ // Names of a few extras defined in VoiceSearch's RecognitionController
+ // Note, the version of voicesearch that shipped in Froyo returns the raw
+ // RecognitionClientAlternates protocol buffer under the key "alternates",
+ // so a VS market update must be installed on Froyo devices in order to see
+ // alternatives.
+ private static final String ALTERNATES_BUNDLE = "alternates_bundle";
+
+ // This is copied from the VoiceSearch app.
+ @SuppressWarnings("unused")
+ private static final class AlternatesBundleKeys {
+ public static final String ALTERNATES = "alternates";
+ public static final String CONFIDENCE = "confidence";
+ public static final String LENGTH = "length";
+ public static final String MAX_SPAN_LENGTH = "max_span_length";
+ public static final String SPANS = "spans";
+ public static final String SPAN_KEY_DELIMITER = ":";
+ public static final String START = "start";
+ public static final String TEXT = "text";
+ }
+
+ // Names of a few intent extras defined in VoiceSearch's RecognitionService.
+ // These let us tweak the endpointer parameters.
+ private static final String EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS =
+ "android.speech.extras.SPEECH_INPUT_MINIMUM_LENGTH_MILLIS";
+ private static final String EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS =
+ "android.speech.extras.SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS";
+ private static final String EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS =
+ "android.speech.extras.SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS";
+
+ // The usual endpointer default value for input complete silence length is 0.5 seconds,
+ // but that's used for things like voice search. For dictation-like voice input like this,
+ // we go with a more liberal value of 1 second. This value will only be used if a value
+ // is not provided from Gservices.
+ private static final String INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS = "1000";
+
+ // Used to record part of that state for logging purposes.
+ public static final int DEFAULT = 0;
+ public static final int LISTENING = 1;
+ public static final int WORKING = 2;
+ public static final int ERROR = 3;
+
+ private int mAfterVoiceInputDeleteCount = 0;
+ private int mAfterVoiceInputInsertCount = 0;
+ private int mAfterVoiceInputInsertPunctuationCount = 0;
+ private int mAfterVoiceInputCursorPos = 0;
+ private int mAfterVoiceInputSelectionSpan = 0;
+
+ private int mState = DEFAULT;
+
+ private final static int MSG_RESET = 1;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_RESET) {
+ mState = DEFAULT;
+ mRecognitionView.finish();
+ mUiListener.onCancelVoice();
+ }
+ }
+ };
+
+ /**
+ * Events relating to the recognition UI. You must implement these.
+ */
+ public interface UiListener {
+
+ /**
+ * @param recognitionResults a set of transcripts for what the user
+ * spoke, sorted by likelihood.
+ */
+ public void onVoiceResults(
+ List<String> recognitionResults,
+ Map<String, List<CharSequence>> alternatives);
+
+ /**
+ * Called when the user cancels speech recognition.
+ */
+ public void onCancelVoice();
+ }
+
+ private SpeechRecognizer mSpeechRecognizer;
+ private RecognitionListener mRecognitionListener;
+ private RecognitionView mRecognitionView;
+ private UiListener mUiListener;
+ private Context mContext;
+
+ /**
+ * @param context the service or activity in which we're running.
+ * @param uiHandler object to receive events from VoiceInput.
+ */
+ public VoiceInput(Context context, UiListener uiHandler) {
+ mLogger = VoiceInputLogger.getLogger(context);
+ mRecognitionListener = new ImeRecognitionListener();
+ mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
+ mSpeechRecognizer.setRecognitionListener(mRecognitionListener);
+ mUiListener = uiHandler;
+ mContext = context;
+ newView();
+
+ String recommendedPackages = SettingsUtil.getSettingsString(
+ context.getContentResolver(),
+ SettingsUtil.LATIN_IME_VOICE_INPUT_RECOMMENDED_PACKAGES,
+ DEFAULT_RECOMMENDED_PACKAGES);
+
+ mRecommendedList = new Whitelist();
+ for (String recommendedPackage : recommendedPackages.split("\\s+")) {
+ mRecommendedList.addApp(recommendedPackage);
+ }
+
+ mBlacklist = new Whitelist();
+ mBlacklist.addApp("com.google.android.setupwizard");
+ }
+
+ public void setCursorPos(int pos) {
+ mAfterVoiceInputCursorPos = pos;
+ }
+
+ public int getCursorPos() {
+ return mAfterVoiceInputCursorPos;
+ }
+
+ public void setSelectionSpan(int span) {
+ mAfterVoiceInputSelectionSpan = span;
+ }
+
+ public int getSelectionSpan() {
+ return mAfterVoiceInputSelectionSpan;
+ }
+
+ public void incrementTextModificationDeleteCount(int count){
+ mAfterVoiceInputDeleteCount += count;
+ // Send up intents for other text modification types
+ if (mAfterVoiceInputInsertCount > 0) {
+ logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
+ mAfterVoiceInputInsertCount = 0;
+ }
+ if (mAfterVoiceInputInsertPunctuationCount > 0) {
+ logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
+ mAfterVoiceInputInsertPunctuationCount = 0;
+ }
+
+ }
+
+ public void incrementTextModificationInsertCount(int count){
+ mAfterVoiceInputInsertCount += count;
+ if (mAfterVoiceInputSelectionSpan > 0) {
+ // If text was highlighted before inserting the char, count this as
+ // a delete.
+ mAfterVoiceInputDeleteCount += mAfterVoiceInputSelectionSpan;
+ }
+ // Send up intents for other text modification types
+ if (mAfterVoiceInputDeleteCount > 0) {
+ logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
+ mAfterVoiceInputDeleteCount = 0;
+ }
+ if (mAfterVoiceInputInsertPunctuationCount > 0) {
+ logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
+ mAfterVoiceInputInsertPunctuationCount = 0;
+ }
+ }
+
+ public void incrementTextModificationInsertPunctuationCount(int count){
+ mAfterVoiceInputInsertPunctuationCount += count;
+ if (mAfterVoiceInputSelectionSpan > 0) {
+ // If text was highlighted before inserting the char, count this as
+ // a delete.
+ mAfterVoiceInputDeleteCount += mAfterVoiceInputSelectionSpan;
+ }
+ // Send up intents for aggregated non-punctuation insertions
+ if (mAfterVoiceInputDeleteCount > 0) {
+ logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
+ mAfterVoiceInputDeleteCount = 0;
+ }
+ if (mAfterVoiceInputInsertCount > 0) {
+ logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
+ mAfterVoiceInputInsertCount = 0;
+ }
+ }
+
+ public void flushAllTextModificationCounters() {
+ if (mAfterVoiceInputInsertCount > 0) {
+ logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
+ mAfterVoiceInputInsertCount = 0;
+ }
+ if (mAfterVoiceInputDeleteCount > 0) {
+ logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
+ mAfterVoiceInputDeleteCount = 0;
+ }
+ if (mAfterVoiceInputInsertPunctuationCount > 0) {
+ logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
+ mAfterVoiceInputInsertPunctuationCount = 0;
+ }
+ }
+
+ /**
+ * The configuration of the IME changed and may have caused the views to be layed out
+ * again. Restore the state of the recognition view.
+ */
+ public void onConfigurationChanged(Configuration configuration) {
+ mRecognitionView.restoreState();
+ mRecognitionView.getView().dispatchConfigurationChanged(configuration);
+ }
+
+ /**
+ * @return true if field is blacklisted for voice
+ */
+ public boolean isBlacklistedField(FieldContext context) {
+ return mBlacklist.matches(context);
+ }
+
+ /**
+ * Used to decide whether to show voice input hints for this field, etc.
+ *
+ * @return true if field is recommended for voice
+ */
+ public boolean isRecommendedField(FieldContext context) {
+ return mRecommendedList.matches(context);
+ }
+
+ /**
+ * 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
+ * responsibility to ensure that the view is visible to the user at this stage.
+ *
+ * @param context the same FieldContext supplied to voiceIsEnabled()
+ * @param swipe whether this voice input was started by swipe, for logging purposes
+ */
+ public void startListening(FieldContext context, boolean swipe) {
+ if (DBG) {
+ Log.d(TAG, "startListening: " + context);
+ }
+
+ if (mState != DEFAULT) {
+ Log.w(TAG, "startListening in the wrong status " + mState);
+ }
+
+ // If everything works ok, the voice input should be already in the correct state. As this
+ // class can be called by third-party, we call reset just to be on the safe side.
+ reset();
+
+ Locale locale = Locale.getDefault();
+ String localeString = locale.getLanguage() + "-" + locale.getCountry();
+
+ mLogger.start(localeString, swipe);
+
+ mState = LISTENING;
+
+ mRecognitionView.showInitializing();
+ startListeningAfterInitialization(context);
+ }
+
+ /**
+ * 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(EXTRA_ALTERNATES, true);
+ intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,
+ SettingsUtil.getSettingsInt(
+ mContext.getContentResolver(),
+ SettingsUtil.LATIN_IME_MAX_VOICE_RESULTS,
+ 1));
+ // Get endpointer params from Gservices.
+ // TODO: Consider caching these values for improved performance on slower devices.
+ final ContentResolver cr = mContext.getContentResolver();
+ putEndpointerExtra(
+ cr,
+ intent,
+ SettingsUtil.LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS,
+ EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS,
+ null /* rely on endpointer default */);
+ putEndpointerExtra(
+ cr,
+ intent,
+ SettingsUtil.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,
+ SettingsUtil.
+ LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
+ EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
+ null /* rely on endpointer default */);
+
+ mSpeechRecognizer.startListening(intent);
+ }
+
+ /**
+ * Gets the value of the provided Gservices key, attempts to parse it into a long,
+ * and if successful, puts the long value as an extra in the provided intent.
+ */
+ private void putEndpointerExtra(ContentResolver cr, Intent i,
+ String gservicesKey, String intentExtraKey, String defaultValue) {
+ long l = -1;
+ String s = SettingsUtil.getSettingsString(cr, gservicesKey, defaultValue);
+ if (s != null) {
+ try {
+ l = Long.valueOf(s);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "could not parse value for " + gservicesKey + ": " + s);
+ }
+ }
+
+ if (l != -1) i.putExtra(intentExtraKey, l);
+ }
+
+ public void destroy() {
+ mSpeechRecognizer.destroy();
+ }
+
+ /**
+ * Creates a new instance of the view that is returned by {@link #getView()}
+ * Clients should use this when a previously returned view is stuck in a
+ * layout that is being thrown away and a new one is need to show to the
+ * user.
+ */
+ public void newView() {
+ mRecognitionView = new RecognitionView(mContext, this);
+ }
+
+ /**
+ * @return a view that shows the recognition flow--e.g., "Speak now" and
+ * "working" dialogs.
+ */
+ public View getView() {
+ return mRecognitionView.getView();
+ }
+
+ /**
+ * Handle the cancel button.
+ */
+ @Override
+ public void onClick(View view) {
+ switch(view.getId()) {
+ case R.id.button:
+ cancel();
+ break;
+ }
+ }
+
+ public void logTextModifiedByTypingInsertion(int length) {
+ mLogger.textModifiedByTypingInsertion(length);
+ }
+
+ public void logTextModifiedByTypingInsertionPunctuation(int length) {
+ mLogger.textModifiedByTypingInsertionPunctuation(length);
+ }
+
+ public void logTextModifiedByTypingDeletion(int length) {
+ mLogger.textModifiedByTypingDeletion(length);
+ }
+
+ public void logTextModifiedByChooseSuggestion(String suggestion, int index,
+ String wordSeparators, InputConnection ic) {
+ String wordToBeReplaced = EditingUtils.getWordAtCursor(ic, wordSeparators);
+ // If we enable phrase-based alternatives, only send up the first word
+ // in suggestion and wordToBeReplaced.
+ mLogger.textModifiedByChooseSuggestion(suggestion.length(), wordToBeReplaced.length(),
+ index, wordToBeReplaced, suggestion);
+ }
+
+ public void logKeyboardWarningDialogShown() {
+ mLogger.keyboardWarningDialogShown();
+ }
+
+ public void logKeyboardWarningDialogDismissed() {
+ mLogger.keyboardWarningDialogDismissed();
+ }
+
+ public void logKeyboardWarningDialogOk() {
+ mLogger.keyboardWarningDialogOk();
+ }
+
+ public void logKeyboardWarningDialogCancel() {
+ mLogger.keyboardWarningDialogCancel();
+ }
+
+ public void logSwipeHintDisplayed() {
+ mLogger.swipeHintDisplayed();
+ }
+
+ public void logPunctuationHintDisplayed() {
+ mLogger.punctuationHintDisplayed();
+ }
+
+ public void logVoiceInputDelivered(int length) {
+ mLogger.voiceInputDelivered(length);
+ }
+
+ public void logInputEnded() {
+ mLogger.inputEnded();
+ }
+
+ public void flushLogs() {
+ mLogger.flush();
+ }
+
+ private static Intent makeIntent() {
+ Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+
+ // On Cupcake, use VoiceIMEHelper since VoiceSearch doesn't support.
+ // On Donut, always use VoiceSearch, since VoiceIMEHelper and
+ // VoiceSearch may conflict.
+ if (Build.VERSION.RELEASE.equals("1.5")) {
+ intent = intent.setClassName(
+ "com.google.android.voiceservice",
+ "com.google.android.voiceservice.IMERecognitionService");
+ } else {
+ intent = intent.setClassName(
+ "com.google.android.voicesearch",
+ "com.google.android.voicesearch.RecognitionService");
+ }
+
+ return intent;
+ }
+
+ /**
+ * Reset the current voice recognition.
+ */
+ public void reset() {
+ if (mState != DEFAULT) {
+ mState = DEFAULT;
+
+ // Remove all pending tasks (e.g., timers to cancel voice input)
+ mHandler.removeMessages(MSG_RESET);
+
+ mSpeechRecognizer.cancel();
+ mRecognitionView.finish();
+ }
+ }
+
+ /**
+ * Cancel in-progress speech recognition.
+ */
+ public void cancel() {
+ switch (mState) {
+ case LISTENING:
+ mLogger.cancelDuringListening();
+ break;
+ case WORKING:
+ mLogger.cancelDuringWorking();
+ break;
+ case ERROR:
+ mLogger.cancelDuringError();
+ break;
+ }
+
+ reset();
+ mUiListener.onCancelVoice();
+ }
+
+ private int getErrorStringId(int errorType, boolean endpointed) {
+ switch (errorType) {
+ // We use CLIENT_ERROR to signify that voice search is not available on the device.
+ case SpeechRecognizer.ERROR_CLIENT:
+ return R.string.voice_not_installed;
+ case SpeechRecognizer.ERROR_NETWORK:
+ return R.string.voice_network_error;
+ case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
+ return endpointed ?
+ R.string.voice_network_error : R.string.voice_too_much_speech;
+ case SpeechRecognizer.ERROR_AUDIO:
+ return R.string.voice_audio_error;
+ case SpeechRecognizer.ERROR_SERVER:
+ return R.string.voice_server_error;
+ case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
+ return R.string.voice_speech_timeout;
+ case SpeechRecognizer.ERROR_NO_MATCH:
+ return R.string.voice_no_match;
+ default: return R.string.voice_error;
+ }
+ }
+
+ private void onError(int errorType, boolean endpointed) {
+ Log.i(TAG, "error " + errorType);
+ mLogger.error(errorType);
+ onError(mContext.getString(getErrorStringId(errorType, endpointed)));
+ }
+
+ private void onError(String error) {
+ mState = ERROR;
+ mRecognitionView.showError(error);
+ // Wait a couple seconds and then automatically dismiss message.
+ mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_RESET), 2000);
+ }
+
+ private class ImeRecognitionListener implements RecognitionListener {
+ // Waveform data
+ final ByteArrayOutputStream mWaveBuffer = new ByteArrayOutputStream();
+ int mSpeechStart;
+ private boolean mEndpointed = false;
+
+ @Override
+ public void onReadyForSpeech(Bundle noiseParams) {
+ mRecognitionView.showListening();
+ }
+
+ @Override
+ public void onBeginningOfSpeech() {
+ mEndpointed = false;
+ mSpeechStart = mWaveBuffer.size();
+ }
+
+ @Override
+ public void onRmsChanged(float rmsdB) {
+ mRecognitionView.updateVoiceMeter(rmsdB);
+ }
+
+ @Override
+ public void onBufferReceived(byte[] buf) {
+ try {
+ mWaveBuffer.write(buf);
+ } catch (IOException e) {
+ // ignore.
+ }
+ }
+
+ @Override
+ public void onEndOfSpeech() {
+ mEndpointed = true;
+ mState = WORKING;
+ mRecognitionView.showWorking(mWaveBuffer, mSpeechStart, mWaveBuffer.size());
+ }
+
+ @Override
+ public void onError(int errorType) {
+ mState = ERROR;
+ VoiceInput.this.onError(errorType, mEndpointed);
+ }
+
+ @Override
+ public void onResults(Bundle resultsBundle) {
+ List<String> results = resultsBundle
+ .getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
+ // VS Market update is needed for IME froyo clients to access the alternatesBundle
+ // TODO: verify this.
+ Bundle alternatesBundle = resultsBundle.getBundle(ALTERNATES_BUNDLE);
+ mState = DEFAULT;
+
+ final Map<String, List<CharSequence>> alternatives =
+ new HashMap<String, List<CharSequence>>();
+
+ if (ENABLE_WORD_CORRECTIONS && alternatesBundle != null && results.size() > 0) {
+ // Use the top recognition result to map each alternative's start:length to a word.
+ String[] words = results.get(0).split(" ");
+ Bundle spansBundle = alternatesBundle.getBundle(AlternatesBundleKeys.SPANS);
+ for (String key : spansBundle.keySet()) {
+ // Get the word for which these alternates correspond to.
+ Bundle spanBundle = spansBundle.getBundle(key);
+ int start = spanBundle.getInt(AlternatesBundleKeys.START);
+ int length = spanBundle.getInt(AlternatesBundleKeys.LENGTH);
+ // Only keep single-word based alternatives.
+ if (length == 1 && start < words.length) {
+ // Get the alternatives associated with the span.
+ // If a word appears twice in a recognition result,
+ // concatenate the alternatives for the word.
+ List<CharSequence> altList = alternatives.get(words[start]);
+ if (altList == null) {
+ altList = new ArrayList<CharSequence>();
+ alternatives.put(words[start], altList);
+ }
+ Parcelable[] alternatesArr = spanBundle
+ .getParcelableArray(AlternatesBundleKeys.ALTERNATES);
+ for (int j = 0; j < alternatesArr.length &&
+ altList.size() < MAX_ALT_LIST_LENGTH; j++) {
+ Bundle alternateBundle = (Bundle) alternatesArr[j];
+ String alternate = alternateBundle.getString(AlternatesBundleKeys.TEXT);
+ // Don't allow duplicates in the alternates list.
+ if (!altList.contains(alternate)) {
+ altList.add(alternate);
+ }
+ }
+ }
+ }
+ }
+
+ if (results.size() > 5) {
+ results = results.subList(0, 5);
+ }
+ mUiListener.onVoiceResults(results, alternatives);
+ mRecognitionView.finish();
+ }
+
+ @Override
+ public void onPartialResults(final Bundle partialResults) {
+ // currently - do nothing
+ }
+
+ @Override
+ public void onEvent(int eventType, Bundle params) {
+ // do nothing - reserved for events that might be added in the future
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java b/java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java
new file mode 100644
index 000000000..22e8207bf
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import com.android.common.speech.LoggingEvents;
+import com.android.inputmethod.deprecated.compat.VoiceInputLoggerCompatUtils;
+
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Provides the logging facility for voice input events. This fires broadcasts back to
+ * the voice search app which then logs on our behalf.
+ *
+ * Note that debug console logging does not occur in this class. If you want to
+ * see console output of these logging events, there is a boolean switch to turn
+ * on on the VoiceSearch side.
+ */
+public class VoiceInputLogger {
+ @SuppressWarnings("unused")
+ private static final String TAG = VoiceInputLogger.class.getSimpleName();
+
+ private static VoiceInputLogger sVoiceInputLogger;
+
+ private final Context mContext;
+
+ // The base intent used to form all broadcast intents to the logger
+ // in VoiceSearch.
+ private final Intent mBaseIntent;
+
+ // This flag is used to indicate when there are voice events that
+ // need to be flushed.
+ private boolean mHasLoggingInfo = false;
+
+ /**
+ * Returns the singleton of the logger.
+ *
+ * @param contextHint a hint context used when creating the logger instance.
+ * Ignored if the singleton instance already exists.
+ */
+ public static synchronized VoiceInputLogger getLogger(Context contextHint) {
+ if (sVoiceInputLogger == null) {
+ sVoiceInputLogger = new VoiceInputLogger(contextHint);
+ }
+ return sVoiceInputLogger;
+ }
+
+ public VoiceInputLogger(Context context) {
+ mContext = context;
+
+ mBaseIntent = new Intent(LoggingEvents.ACTION_LOG_EVENT);
+ mBaseIntent.putExtra(LoggingEvents.EXTRA_APP_NAME, LoggingEvents.VoiceIme.APP_NAME);
+ }
+
+ private Intent newLoggingBroadcast(int event) {
+ Intent i = new Intent(mBaseIntent);
+ i.putExtra(LoggingEvents.EXTRA_EVENT, event);
+ return i;
+ }
+
+ public void flush() {
+ if (hasLoggingInfo()) {
+ Intent i = new Intent(mBaseIntent);
+ i.putExtra(LoggingEvents.EXTRA_FLUSH, true);
+ mContext.sendBroadcast(i);
+ setHasLoggingInfo(false);
+ }
+ }
+
+ public void keyboardWarningDialogShown() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_SHOWN));
+ }
+
+ public void keyboardWarningDialogDismissed() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_DISMISSED));
+ }
+
+ public void keyboardWarningDialogOk() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_OK));
+ }
+
+ public void keyboardWarningDialogCancel() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_CANCEL));
+ }
+
+ public void settingsWarningDialogShown() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_SHOWN));
+ }
+
+ public void settingsWarningDialogDismissed() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_DISMISSED));
+ }
+
+ public void settingsWarningDialogOk() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_OK));
+ }
+
+ public void settingsWarningDialogCancel() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_CANCEL));
+ }
+
+ public void swipeHintDisplayed() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.SWIPE_HINT_DISPLAYED));
+ }
+
+ public void cancelDuringListening() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_LISTENING));
+ }
+
+ public void cancelDuringWorking() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_WORKING));
+ }
+
+ public void cancelDuringError() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_ERROR));
+ }
+
+ public void punctuationHintDisplayed() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.PUNCTUATION_HINT_DISPLAYED));
+ }
+
+ public void error(int code) {
+ setHasLoggingInfo(true);
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.ERROR);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_ERROR_CODE, code);
+ mContext.sendBroadcast(i);
+ }
+
+ public void start(String locale, boolean swipe) {
+ setHasLoggingInfo(true);
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.START);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_START_LOCALE, locale);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_START_SWIPE, swipe);
+ i.putExtra(LoggingEvents.EXTRA_TIMESTAMP, System.currentTimeMillis());
+ mContext.sendBroadcast(i);
+ }
+
+ public void voiceInputDelivered(int length) {
+ setHasLoggingInfo(true);
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.VOICE_INPUT_DELIVERED);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length);
+ mContext.sendBroadcast(i);
+ }
+
+ public void textModifiedByTypingInsertion(int length) {
+ setHasLoggingInfo(true);
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE,
+ LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_TYPING_INSERTION);
+ mContext.sendBroadcast(i);
+ }
+
+ public void textModifiedByTypingInsertionPunctuation(int length) {
+ setHasLoggingInfo(true);
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE,
+ LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_TYPING_INSERTION_PUNCTUATION);
+ mContext.sendBroadcast(i);
+ }
+
+ public void textModifiedByTypingDeletion(int length) {
+ setHasLoggingInfo(true);
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE,
+ LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_TYPING_DELETION);
+
+ mContext.sendBroadcast(i);
+ }
+
+
+ public void textModifiedByChooseSuggestion(int suggestionLength, int replacedPhraseLength,
+ int index, String before, String after) {
+ setHasLoggingInfo(true);
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, suggestionLength);
+ i.putExtra(VoiceInputLoggerCompatUtils.EXTRA_TEXT_REPLACED_LENGTH, replacedPhraseLength);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE,
+ LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_CHOOSE_SUGGESTION);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_N_BEST_CHOOSE_INDEX, index);
+ i.putExtra(VoiceInputLoggerCompatUtils.EXTRA_BEFORE_N_BEST_CHOOSE, before);
+ i.putExtra(VoiceInputLoggerCompatUtils.EXTRA_AFTER_N_BEST_CHOOSE, after);
+ mContext.sendBroadcast(i);
+ }
+
+ public void inputEnded() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.INPUT_ENDED));
+ }
+
+ public void voiceInputSettingEnabled() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.VOICE_INPUT_SETTING_ENABLED));
+ }
+
+ public void voiceInputSettingDisabled() {
+ setHasLoggingInfo(true);
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.VOICE_INPUT_SETTING_DISABLED));
+ }
+
+ private void setHasLoggingInfo(boolean hasLoggingInfo) {
+ mHasLoggingInfo = hasLoggingInfo;
+ // If applications that call UserHappinessSignals.userAcceptedImeText
+ // make that call after VoiceInputLogger.flush() calls this method with false, we
+ // will lose those happiness signals. For example, consider the gmail sequence:
+ // 1. compose message
+ // 2. speak message into message field
+ // 3. type subject into subject field
+ // 4. press send
+ // We will NOT get the signal that the user accepted the voice inputted message text
+ // because when the user tapped on the subject field, the ime's flush will be triggered
+ // and the hasLoggingInfo will be then set to false. So by the time the user hits send
+ // we have essentially forgotten about any voice input.
+ // However the following (more common) use case is properly logged
+ // 1. compose message
+ // 2. type subject in subject field
+ // 3. speak message in message field
+ // 4. press send
+ VoiceInputLoggerCompatUtils.setHasVoiceLoggingInfoCompat(hasLoggingInfo);
+ }
+
+ private boolean hasLoggingInfo(){
+ return mHasLoggingInfo;
+ }
+
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java b/java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java
new file mode 100644
index 000000000..8ed279f42
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2008-2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+
+/**
+ * Utility class to draw a waveform into a bitmap, given a byte array
+ * that represents the waveform as a sequence of 16-bit integers.
+ * Adapted from RecognitionActivity.java.
+ */
+public class WaveformImage {
+ private static final int SAMPLING_RATE = 8000;
+
+ private WaveformImage() {
+ // Intentional empty constructor.
+ }
+
+ public static Bitmap drawWaveform(
+ ByteArrayOutputStream waveBuffer, int w, int h, int start, int end) {
+ final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ final Canvas c = new Canvas(b);
+ final Paint paint = new Paint();
+ paint.setColor(0xFFFFFFFF); // 0xRRGGBBAA
+ paint.setAntiAlias(true);
+ paint.setStrokeWidth(0);
+
+ final ShortBuffer buf = ByteBuffer
+ .wrap(waveBuffer.toByteArray())
+ .order(ByteOrder.nativeOrder())
+ .asShortBuffer();
+ buf.position(0);
+
+ final int numSamples = waveBuffer.size() / 2;
+ final int delay = (SAMPLING_RATE * 100 / 1000);
+ int endIndex = end / 2 + delay;
+ if (end == 0 || endIndex >= numSamples) {
+ endIndex = numSamples;
+ }
+ int index = start / 2 - delay;
+ if (index < 0) {
+ index = 0;
+ }
+ final int size = endIndex - index;
+ int numSamplePerPixel = 32;
+ int delta = size / (numSamplePerPixel * w);
+ if (delta == 0) {
+ numSamplePerPixel = size / w;
+ delta = 1;
+ }
+
+ final float scale = 3.5f / 65536.0f;
+ // do one less column to make sure we won't read past
+ // the buffer.
+ try {
+ for (int i = 0; i < w - 1 ; i++) {
+ final float x = i;
+ for (int j = 0; j < numSamplePerPixel; j++) {
+ final short s = buf.get(index);
+ final float y = (h / 2) - (s * h * scale);
+ c.drawPoint(x, y, paint);
+ index += delta;
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ // this can happen, but we don't care
+ }
+
+ return b;
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/Whitelist.java b/java/src/com/android/inputmethod/deprecated/voice/Whitelist.java
new file mode 100644
index 000000000..6c5f52ae2
--- /dev/null
+++ b/java/src/com/android/inputmethod/deprecated/voice/Whitelist.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.deprecated.voice;
+
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A set of text fields where speech has been explicitly enabled.
+ */
+public class Whitelist {
+ private List<Bundle> mConditions;
+
+ public Whitelist() {
+ mConditions = new ArrayList<Bundle>();
+ }
+
+ public Whitelist(List<Bundle> conditions) {
+ this.mConditions = conditions;
+ }
+
+ public void addApp(String app) {
+ Bundle bundle = new Bundle();
+ bundle.putString("packageName", app);
+ mConditions.add(bundle);
+ }
+
+ /**
+ * @return true if the field is a member of the whitelist.
+ */
+ public boolean matches(FieldContext context) {
+ for (Bundle condition : mConditions) {
+ if (matches(condition, context.getBundle())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return true of all values in condition are matched by a value
+ * in target.
+ */
+ private boolean matches(Bundle condition, Bundle target) {
+ for (String key : condition.keySet()) {
+ if (!condition.getString(key).equals(target.getString(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}