aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java133
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java129
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java37
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java201
-rw-r--r--java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java226
-rw-r--r--java/src/com/android/inputmethod/compat/AbstractCompatWrapper.java39
-rw-r--r--java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java39
-rw-r--r--java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java42
-rw-r--r--java/src/com/android/inputmethod/compat/ArraysCompatUtils.java50
-rw-r--r--java/src/com/android/inputmethod/compat/CompatUtils.java156
-rw-r--r--java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java102
-rw-r--r--java/src/com/android/inputmethod/compat/FrameLayoutCompatUtils.java63
-rw-r--r--java/src/com/android/inputmethod/compat/InputConnectionCompatUtils.java80
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodInfoCompatWrapper.java59
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java227
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java84
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatWrapper.java161
-rw-r--r--java/src/com/android/inputmethod/compat/InputTypeCompatUtils.java118
-rw-r--r--java/src/com/android/inputmethod/compat/LinearLayoutCompatUtils.java55
-rw-r--r--java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java23
-rw-r--r--java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java87
-rw-r--r--java/src/com/android/inputmethod/compat/VibratorCompatWrapper.java47
-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.java (renamed from java/src/com/android/inputmethod/latin/LanguageSwitcher.java)133
-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.java (renamed from java/src/com/android/inputmethod/voice/FieldContext.java)6
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/Hints.java (renamed from java/src/com/android/inputmethod/latin/Hints.java)39
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java (renamed from java/src/com/android/inputmethod/voice/RecognitionView.java)255
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java (renamed from java/src/com/android/inputmethod/voice/SettingsUtil.java)7
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java155
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java (renamed from java/src/com/android/inputmethod/voice/VoiceInput.java)82
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java (renamed from java/src/com/android/inputmethod/voice/VoiceInputLogger.java)20
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java (renamed from java/src/com/android/inputmethod/voice/WaveformImage.java)8
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/Whitelist.java (renamed from java/src/com/android/inputmethod/voice/Whitelist.java)5
-rw-r--r--java/src/com/android/inputmethod/keyboard/Key.java474
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyDetector.java202
-rw-r--r--java/src/com/android/inputmethod/keyboard/Keyboard.java461
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java78
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardId.java220
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java849
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardView.java1364
-rw-r--r--java/src/com/android/inputmethod/keyboard/LatinKeyboard.java425
-rw-r--r--java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java242
-rw-r--r--java/src/com/android/inputmethod/keyboard/MiniKeyboard.java52
-rw-r--r--java/src/com/android/inputmethod/keyboard/MiniKeyboardKeyDetector.java (renamed from java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java)42
-rw-r--r--java/src/com/android/inputmethod/keyboard/PointerTracker.java709
-rw-r--r--java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java112
-rw-r--r--java/src/com/android/inputmethod/keyboard/PopupPanel.java45
-rw-r--r--java/src/com/android/inputmethod/keyboard/ProximityInfo.java80
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java241
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java146
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java726
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardShiftState.java135
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java250
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java85
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PointerTrackerKeyState.java101
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java85
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PopupCharactersParser.java185
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/Row.java73
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java69
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/SlidingLocaleDrawable.java161
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/SwipeTracker.java (renamed from java/src/com/android/inputmethod/latin/SwipeTracker.java)10
-rw-r--r--java/src/com/android/inputmethod/latin/AssetFileAddress.java52
-rw-r--r--java/src/com/android/inputmethod/latin/AutoCorrection.java143
-rw-r--r--java/src/com/android/inputmethod/latin/AutoDictionary.java27
-rw-r--r--java/src/com/android/inputmethod/latin/BackupAgent.java (renamed from java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java292
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java151
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java97
-rw-r--r--[-rwxr-xr-x]java/src/com/android/inputmethod/latin/CandidateView.java806
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsDictionary.java24
-rw-r--r--java/src/com/android/inputmethod/latin/DebugSettings.java (renamed from java/src/com/android/inputmethod/latin/LatinIMEDebugSettings.java)23
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java27
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryCollection.java71
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java177
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java89
-rw-r--r--java/src/com/android/inputmethod/latin/EditingUtils.java (renamed from java/src/com/android/inputmethod/latin/EditingUtil.java)236
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java308
-rw-r--r--java/src/com/android/inputmethod/latin/Flag.java64
-rw-r--r--java/src/com/android/inputmethod/latin/InputLanguageSelection.java216
-rw-r--r--java/src/com/android/inputmethod/latin/KeyDetector.java113
-rw-r--r--java/src/com/android/inputmethod/latin/KeyboardSwitcher.java606
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java2942
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIMESettings.java204
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIMEUtil.java171
-rw-r--r--java/src/com/android/inputmethod/latin/LatinImeLogger.java17
-rw-r--r--java/src/com/android/inputmethod/latin/LatinKeyboard.java1027
-rw-r--r--java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java1517
-rw-r--r--java/src/com/android/inputmethod/latin/LatinKeyboardView.java380
-rw-r--r--java/src/com/android/inputmethod/latin/PointerTracker.java581
-rw-r--r--java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java (renamed from java/src/com/android/inputmethod/latin/ModifierKeyState.java)29
-rw-r--r--java/src/com/android/inputmethod/latin/ProximityKeyDetector.java86
-rw-r--r--java/src/com/android/inputmethod/latin/Settings.java603
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeLocale.java46
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java674
-rw-r--r--[-rwxr-xr-x]java/src/com/android/inputmethod/latin/Suggest.java496
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java184
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java43
-rw-r--r--java/src/com/android/inputmethod/latin/TextEntryState.java382
-rw-r--r--java/src/com/android/inputmethod/latin/Tutorial.java244
-rw-r--r--java/src/com/android/inputmethod/latin/UserBigramDictionary.java40
-rw-r--r--java/src/com/android/inputmethod/latin/UserDictionary.java48
-rw-r--r--java/src/com/android/inputmethod/latin/Utils.java718
-rw-r--r--java/src/com/android/inputmethod/latin/WhitelistDictionary.java100
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java66
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java115
110 files changed, 17839 insertions, 8460 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
new file mode 100644
index 000000000..ae614b7e0
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
@@ -0,0 +1,133 @@
+/*
+ * 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.accessibility;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.inputmethodservice.InputMethodService;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
+import com.android.inputmethod.compat.AccessibilityManagerCompatWrapper;
+import com.android.inputmethod.compat.MotionEventCompatUtils;
+
+public class AccessibilityUtils {
+ private static final String TAG = AccessibilityUtils.class.getSimpleName();
+ private static final String CLASS = AccessibilityUtils.class.getClass().getName();
+ private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage()
+ .getName();
+
+ private static final AccessibilityUtils sInstance = new AccessibilityUtils();
+
+ private AccessibilityManager mAccessibilityManager;
+ private AccessibilityManagerCompatWrapper mCompatManager;
+
+ /*
+ * Setting this constant to {@code false} will disable all keyboard
+ * accessibility code, regardless of whether Accessibility is turned on in
+ * the system settings. It should ONLY be used in the event of an emergency.
+ */
+ private static final boolean ENABLE_ACCESSIBILITY = true;
+
+ public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
+ if (!ENABLE_ACCESSIBILITY)
+ return;
+
+ // These only need to be initialized if the kill switch is off.
+ sInstance.initInternal(inputMethod, prefs);
+ KeyCodeDescriptionMapper.init(inputMethod, prefs);
+ AccessibleInputMethodServiceProxy.init(inputMethod, prefs);
+ AccessibleKeyboardViewProxy.init(inputMethod, prefs);
+ }
+
+ public static AccessibilityUtils getInstance() {
+ return sInstance;
+ }
+
+ private AccessibilityUtils() {
+ // This class is not publicly instantiable.
+ }
+
+ private void initInternal(Context context, SharedPreferences prefs) {
+ mAccessibilityManager = (AccessibilityManager) context
+ .getSystemService(Context.ACCESSIBILITY_SERVICE);
+ mCompatManager = new AccessibilityManagerCompatWrapper(mAccessibilityManager);
+ }
+
+ /**
+ * Returns {@code true} if touch exploration is enabled. Currently, this
+ * means that the kill switch is off, the device supports touch exploration,
+ * and a spoken feedback service is turned on.
+ *
+ * @return {@code true} if touch exploration is enabled.
+ */
+ public boolean isTouchExplorationEnabled() {
+ return ENABLE_ACCESSIBILITY
+ && AccessibilityEventCompatUtils.supportsTouchExploration()
+ && mAccessibilityManager.isEnabled()
+ && !mCompatManager.getEnabledAccessibilityServiceList(
+ AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty();
+ }
+
+ /**
+ * Returns {@true} if the provided event is a touch exploration (e.g. hover)
+ * event. This is used to determine whether the event should be processed by
+ * the touch exploration code within the keyboard.
+ *
+ * @param event The event to check.
+ * @return {@true} is the event is a touch exploration event
+ */
+ public boolean isTouchExplorationEvent(MotionEvent event) {
+ final int action = event.getAction();
+
+ return action == MotionEventCompatUtils.ACTION_HOVER_ENTER
+ || action == MotionEventCompatUtils.ACTION_HOVER_EXIT
+ || action == MotionEventCompatUtils.ACTION_HOVER_MOVE;
+ }
+
+ /**
+ * Sends the specified text to the {@link AccessibilityManager} to be
+ * spoken.
+ *
+ * @param text the text to speak
+ */
+ public void speak(CharSequence text) {
+ if (!mAccessibilityManager.isEnabled()) {
+ Log.e(TAG, "Attempted to speak when accessibility was disabled!");
+ return;
+ }
+
+ // The following is a hack to avoid using the heavy-weight TextToSpeech
+ // class. Instead, we're just forcing a fake AccessibilityEvent into
+ // the screen reader to make it speak.
+ final AccessibilityEvent event = AccessibilityEvent
+ .obtain(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER);
+
+ event.setPackageName(PACKAGE);
+ event.setClassName(CLASS);
+ event.setEventTime(SystemClock.uptimeMillis());
+ event.setEnabled(true);
+ event.getText().add(text);
+
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
new file mode 100644
index 000000000..043266c70
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
@@ -0,0 +1,129 @@
+/*
+ * 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.accessibility;
+
+import android.content.SharedPreferences;
+import android.inputmethodservice.InputMethodService;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+
+import com.android.inputmethod.latin.R;
+
+public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActionListener {
+ private static final AccessibleInputMethodServiceProxy sInstance =
+ new AccessibleInputMethodServiceProxy();
+
+ /*
+ * Delay for the handler event that's fired when Accessibility is on and the
+ * user hovers outside of any valid keys. This is used to let the user know
+ * that if they lift their finger, nothing will be typed.
+ */
+ private static final long DELAY_NO_HOVER_SELECTION = 250;
+
+ private InputMethodService mInputMethod;
+
+ private AccessibilityHandler mAccessibilityHandler;
+
+ private class AccessibilityHandler extends Handler {
+ private static final int MSG_NO_HOVER_SELECTION = 0;
+
+ public AccessibilityHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_NO_HOVER_SELECTION:
+ notifyNoHoverSelection();
+ break;
+ }
+ }
+
+ public void postNoHoverSelection() {
+ removeMessages(MSG_NO_HOVER_SELECTION);
+ sendEmptyMessageDelayed(MSG_NO_HOVER_SELECTION, DELAY_NO_HOVER_SELECTION);
+ }
+
+ public void cancelNoHoverSelection() {
+ removeMessages(MSG_NO_HOVER_SELECTION);
+ }
+ }
+
+ public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
+ sInstance.initInternal(inputMethod, prefs);
+ }
+
+ public static AccessibleInputMethodServiceProxy getInstance() {
+ return sInstance;
+ }
+
+ private AccessibleInputMethodServiceProxy() {
+ // Not publicly instantiable.
+ }
+
+ private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) {
+ mInputMethod = inputMethod;
+ mAccessibilityHandler = new AccessibilityHandler(inputMethod.getMainLooper());
+ }
+
+ /**
+ * If touch exploration is enabled, cancels the event sent by
+ * {@link AccessibleInputMethodServiceProxy#onHoverExit(int)} because the
+ * user is currently hovering above a key.
+ */
+ @Override
+ public void onHoverEnter(int primaryCode) {
+ mAccessibilityHandler.cancelNoHoverSelection();
+ }
+
+ /**
+ * If touch exploration is enabled, sends a delayed event to notify the user
+ * that they are not currently hovering above a key.
+ */
+ @Override
+ public void onHoverExit(int primaryCode) {
+ mAccessibilityHandler.postNoHoverSelection();
+ }
+
+ /**
+ * When Accessibility is turned on, notifies the user that they are not
+ * currently hovering above a key. By default this will speak the currently
+ * entered text.
+ */
+ private void notifyNoHoverSelection() {
+ final ExtractedText extracted = mInputMethod.getCurrentInputConnection().getExtractedText(
+ new ExtractedTextRequest(), 0);
+
+ if (extracted == null)
+ return;
+
+ final CharSequence text;
+
+ if (TextUtils.isEmpty(extracted.text)) {
+ text = mInputMethod.getString(R.string.spoken_no_text_entered);
+ } else {
+ text = mInputMethod.getString(R.string.spoken_current_text_is, extracted.text);
+ }
+
+ AccessibilityUtils.getInstance().speak(text);
+ }
+}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java
new file mode 100644
index 000000000..12c59d0fc
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java
@@ -0,0 +1,37 @@
+/*
+ * 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.accessibility;
+
+public interface AccessibleKeyboardActionListener {
+ /**
+ * Called when the user hovers inside a key. This is sent only when
+ * Accessibility is turned on. For keys that repeat, this is only called
+ * once.
+ *
+ * @param primaryCode the code of the key that was hovered over
+ */
+ public void onHoverEnter(int primaryCode);
+
+ /**
+ * Called when the user hovers outside a key. This is sent only when
+ * Accessibility is turned on. For keys that repeat, this is only called
+ * once.
+ *
+ * @param primaryCode the code of the key that was hovered over
+ */
+ public void onHoverExit(int primaryCode);
+}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
new file mode 100644
index 000000000..96f7fc9f2
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -0,0 +1,201 @@
+/*
+ * 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.accessibility;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
+import com.android.inputmethod.compat.MotionEventCompatUtils;
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.KeyDetector;
+import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.keyboard.PointerTracker;
+
+public class AccessibleKeyboardViewProxy {
+ private static final String TAG = AccessibleKeyboardViewProxy.class.getSimpleName();
+ private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy();
+
+ // Delay in milliseconds between key press DOWN and UP events
+ private static final long DELAY_KEY_PRESS = 10;
+
+ private int mScaledEdgeSlop;
+ private KeyboardView mView;
+ private AccessibleKeyboardActionListener mListener;
+
+ private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
+ private int mLastX = -1;
+ private int mLastY = -1;
+
+ public static void init(Context context, SharedPreferences prefs) {
+ sInstance.initInternal(context, prefs);
+ sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance();
+ }
+
+ public static AccessibleKeyboardViewProxy getInstance() {
+ return sInstance;
+ }
+
+ public static void setView(KeyboardView view) {
+ sInstance.mView = view;
+ }
+
+ private AccessibleKeyboardViewProxy() {
+ // Not publicly instantiable.
+ }
+
+ private void initInternal(Context context, SharedPreferences prefs) {
+ final Paint paint = new Paint();
+ paint.setTextAlign(Paint.Align.LEFT);
+ paint.setTextSize(14.0f);
+ paint.setAntiAlias(true);
+ paint.setColor(Color.YELLOW);
+
+ mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
+ }
+
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event,
+ PointerTracker tracker) {
+ if (mView == null) {
+ Log.e(TAG, "No keyboard view set!");
+ return false;
+ }
+
+ switch (event.getEventType()) {
+ case AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER:
+ final Key key = tracker.getKey(mLastHoverKeyIndex);
+
+ if (key == null)
+ break;
+
+ final CharSequence description = KeyCodeDescriptionMapper.getInstance()
+ .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key);
+
+ if (description == null)
+ return false;
+
+ event.getText().add(description);
+
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Receives hover events when accessibility is turned on in API > 11. In
+ * earlier API levels, events are manually routed from onTouchEvent.
+ *
+ * @param event The hover event.
+ * @return {@code true} if the event is handled
+ */
+ public boolean onHoverEvent(MotionEvent event, PointerTracker tracker) {
+ return onTouchExplorationEvent(event, tracker);
+ }
+
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ // Since touch exploration translates hover double-tap to a regular
+ // single-tap, we're going to drop non-touch exploration events.
+ if (!AccessibilityUtils.getInstance().isTouchExplorationEvent(event))
+ return true;
+
+ return false;
+ }
+
+ /**
+ * Handles touch exploration events when Accessibility is turned on.
+ *
+ * @param event The touch exploration hover event.
+ * @return {@code true} if the event was handled
+ */
+ private boolean onTouchExplorationEvent(MotionEvent event, PointerTracker tracker) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ switch (event.getAction()) {
+ case MotionEventCompatUtils.ACTION_HOVER_ENTER:
+ case MotionEventCompatUtils.ACTION_HOVER_MOVE:
+ final int keyIndex = tracker.getKeyIndexOn(x, y);
+
+ if (keyIndex != mLastHoverKeyIndex) {
+ fireKeyHoverEvent(tracker, mLastHoverKeyIndex, false);
+ mLastHoverKeyIndex = keyIndex;
+ mLastX = x;
+ mLastY = y;
+ fireKeyHoverEvent(tracker, mLastHoverKeyIndex, true);
+ }
+
+ return true;
+ case MotionEventCompatUtils.ACTION_HOVER_EXIT:
+ final int width = mView.getWidth();
+ final int height = mView.getHeight();
+
+ if (x < mScaledEdgeSlop || y < mScaledEdgeSlop || x >= (width - mScaledEdgeSlop)
+ || y >= (height - mScaledEdgeSlop)) {
+ fireKeyHoverEvent(tracker, mLastHoverKeyIndex, false);
+ mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
+ mLastX = -1;
+ mLastY = -1;
+ } else if (mLastHoverKeyIndex != KeyDetector.NOT_A_KEY) {
+ fireKeyPressEvent(tracker, mLastX, mLastY, event.getEventTime());
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private void fireKeyHoverEvent(PointerTracker tracker, int keyIndex, boolean entering) {
+ if (mListener == null) {
+ Log.e(TAG, "No accessible keyboard action listener set!");
+ return;
+ }
+
+ if (mView == null) {
+ Log.e(TAG, "No keyboard view set!");
+ return;
+ }
+
+ if (keyIndex == KeyDetector.NOT_A_KEY)
+ return;
+
+ final Key key = tracker.getKey(keyIndex);
+
+ if (key == null)
+ return;
+
+ if (entering) {
+ mListener.onHoverEnter(key.mCode);
+ mView.sendAccessibilityEvent(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER);
+ } else {
+ mListener.onHoverExit(key.mCode);
+ mView.sendAccessibilityEvent(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_EXIT);
+ }
+ }
+
+ private void fireKeyPressEvent(PointerTracker tracker, int x, int y, long eventTime) {
+ tracker.onDownEvent(x, y, eventTime, null);
+ tracker.onUpEvent(x, y, eventTime + DELAY_KEY_PRESS, null);
+ }
+}
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
new file mode 100644
index 000000000..154f4af91
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -0,0 +1,226 @@
+/*
+ * 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.accessibility;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.R;
+
+import java.util.HashMap;
+
+public class KeyCodeDescriptionMapper {
+ private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
+
+ // Map of key labels to spoken description resource IDs
+ private final HashMap<CharSequence, Integer> mKeyLabelMap;
+
+ // Map of key codes to spoken description resource IDs
+ private final HashMap<Integer, Integer> mKeyCodeMap;
+
+ // Map of shifted key codes to spoken description resource IDs
+ private final HashMap<Integer, Integer> mShiftedKeyCodeMap;
+
+ // Map of shift-locked key codes to spoken description resource IDs
+ private final HashMap<Integer, Integer> mShiftLockedKeyCodeMap;
+
+ public static void init(Context context, SharedPreferences prefs) {
+ sInstance.initInternal(context, prefs);
+ }
+
+ public static KeyCodeDescriptionMapper getInstance() {
+ return sInstance;
+ }
+
+ private KeyCodeDescriptionMapper() {
+ mKeyLabelMap = new HashMap<CharSequence, Integer>();
+ mKeyCodeMap = new HashMap<Integer, Integer>();
+ mShiftedKeyCodeMap = new HashMap<Integer, Integer>();
+ mShiftLockedKeyCodeMap = new HashMap<Integer, Integer>();
+ }
+
+ private void initInternal(Context context, SharedPreferences prefs) {
+ // Manual label substitutions for key labels with no string resource
+ mKeyLabelMap.put(":-)", R.string.spoken_description_smiley);
+
+ // Symbols that most TTS engines can't speak
+ mKeyCodeMap.put((int) '.', R.string.spoken_description_period);
+ mKeyCodeMap.put((int) ',', R.string.spoken_description_comma);
+ mKeyCodeMap.put((int) '(', R.string.spoken_description_left_parenthesis);
+ mKeyCodeMap.put((int) ')', R.string.spoken_description_right_parenthesis);
+ mKeyCodeMap.put((int) ':', R.string.spoken_description_colon);
+ mKeyCodeMap.put((int) ';', R.string.spoken_description_semicolon);
+ mKeyCodeMap.put((int) '!', R.string.spoken_description_exclamation_mark);
+ mKeyCodeMap.put((int) '?', R.string.spoken_description_question_mark);
+ mKeyCodeMap.put((int) '\"', R.string.spoken_description_double_quote);
+ mKeyCodeMap.put((int) '\'', R.string.spoken_description_single_quote);
+ mKeyCodeMap.put((int) '*', R.string.spoken_description_star);
+ mKeyCodeMap.put((int) '#', R.string.spoken_description_pound);
+ mKeyCodeMap.put((int) ' ', R.string.spoken_description_space);
+
+ // Non-ASCII symbols (must use escape codes!)
+ mKeyCodeMap.put((int) '\u2022', R.string.spoken_description_dot);
+ mKeyCodeMap.put((int) '\u221A', R.string.spoken_description_square_root);
+ mKeyCodeMap.put((int) '\u03C0', R.string.spoken_description_pi);
+ mKeyCodeMap.put((int) '\u0394', R.string.spoken_description_delta);
+ mKeyCodeMap.put((int) '\u2122', R.string.spoken_description_trademark);
+ mKeyCodeMap.put((int) '\u2105', R.string.spoken_description_care_of);
+ mKeyCodeMap.put((int) '\u2026', R.string.spoken_description_ellipsis);
+ mKeyCodeMap.put((int) '\u201E', R.string.spoken_description_low_double_quote);
+
+ // Special non-character codes defined in Keyboard
+ mKeyCodeMap.put(Keyboard.CODE_DELETE, R.string.spoken_description_delete);
+ mKeyCodeMap.put(Keyboard.CODE_ENTER, R.string.spoken_description_return);
+ mKeyCodeMap.put(Keyboard.CODE_SETTINGS, R.string.spoken_description_settings);
+ mKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_shift);
+ mKeyCodeMap.put(Keyboard.CODE_SHORTCUT, R.string.spoken_description_mic);
+ mKeyCodeMap.put(Keyboard.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
+ mKeyCodeMap.put(Keyboard.CODE_TAB, R.string.spoken_description_tab);
+
+ // Shifted versions of non-character codes defined in Keyboard
+ mShiftedKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_shift_shifted);
+
+ // Shift-locked versions of non-character codes defined in Keyboard
+ mShiftLockedKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_caps_lock);
+ }
+
+ /**
+ * Returns the localized description of the action performed by a specified
+ * key based on the current keyboard state.
+ * <p>
+ * The order of precedence for key descriptions is:
+ * <ol>
+ * <li>Manually-defined based on the key label</li>
+ * <li>Automatic or manually-defined based on the key code</li>
+ * <li>Automatically based on the key label</li>
+ * <li>{code null} for keys with no label or key code defined</li>
+ * </p>
+ *
+ * @param context The package's context.
+ * @param keyboard The keyboard on which the key resides.
+ * @param key The key from which to obtain a description.
+ * @return a character sequence describing the action performed by pressing
+ * the key
+ */
+ public CharSequence getDescriptionForKey(Context context, Keyboard keyboard, Key key) {
+ if (key.mCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
+ final CharSequence description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
+ if (description != null)
+ return description;
+ }
+
+ if (!TextUtils.isEmpty(key.mLabel)) {
+ final String label = key.mLabel.toString().trim();
+
+ if (mKeyLabelMap.containsKey(label)) {
+ return context.getString(mKeyLabelMap.get(label));
+ } else if (label.length() == 1
+ || (keyboard.isManualTemporaryUpperCase() && !TextUtils
+ .isEmpty(key.mHintLetter))) {
+ return getDescriptionForKeyCode(context, keyboard, key);
+ } else {
+ return label;
+ }
+ } else if (key.mCode != Keyboard.CODE_DUMMY) {
+ return getDescriptionForKeyCode(context, keyboard, key);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
+ * key or {@code null} if there is not a description provided for the
+ * current keyboard context.
+ *
+ * @param context The package's context.
+ * @param keyboard The keyboard on which the key resides.
+ * @return a character sequence describing the action performed by pressing
+ * the key
+ */
+ private CharSequence getDescriptionForSwitchAlphaSymbol(Context context, Keyboard keyboard) {
+ final KeyboardId id = keyboard.mId;
+
+ if (id.isAlphabetKeyboard()) {
+ return context.getString(R.string.spoken_description_to_symbol);
+ } else if (id.isSymbolsKeyboard()) {
+ return context.getString(R.string.spoken_description_to_alpha);
+ } else if (id.isPhoneSymbolsKeyboard()) {
+ return context.getString(R.string.spoken_description_to_numeric);
+ } else if (id.isPhoneKeyboard()) {
+ return context.getString(R.string.spoken_description_to_symbol);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the keycode for the specified key given the current keyboard
+ * state.
+ *
+ * @param keyboard The keyboard on which the key resides.
+ * @param key The key from which to obtain a key code.
+ * @return the key code for the specified key
+ */
+ private int getCorrectKeyCode(Keyboard keyboard, Key key) {
+ if (keyboard.isManualTemporaryUpperCase() && !TextUtils.isEmpty(key.mHintLetter)) {
+ return key.mHintLetter.charAt(0);
+ } else {
+ return key.mCode;
+ }
+ }
+
+ /**
+ * Returns a localized character sequence describing what will happen when
+ * the specified key is pressed based on its key code.
+ * <p>
+ * The order of precedence for key code descriptions is:
+ * <ol>
+ * <li>Manually-defined shift-locked description</li>
+ * <li>Manually-defined shifted description</li>
+ * <li>Manually-defined normal description</li>
+ * <li>Automatic based on the character represented by the key code</li>
+ * <li>Fall-back for undefined or control characters</li>
+ * </ol>
+ * </p>
+ *
+ * @param context The package's context.
+ * @param keyboard The keyboard on which the key resides.
+ * @param key The key from which to obtain a description.
+ * @return a character sequence describing the action performed by pressing
+ * the key
+ */
+ private CharSequence getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key) {
+ final int code = getCorrectKeyCode(keyboard, key);
+
+ if (keyboard.isShiftLocked() && mShiftLockedKeyCodeMap.containsKey(code)) {
+ return context.getString(mShiftLockedKeyCodeMap.get(code));
+ } else if (keyboard.isShiftedOrShiftLocked() && mShiftedKeyCodeMap.containsKey(code)) {
+ return context.getString(mShiftedKeyCodeMap.get(code));
+ } else if (mKeyCodeMap.containsKey(code)) {
+ return context.getString(mKeyCodeMap.get(code));
+ } else if (Character.isDefined(code) && !Character.isISOControl(code)) {
+ return Character.toString((char) code);
+ } else {
+ return context.getString(R.string.spoken_description_unknown, code);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/AbstractCompatWrapper.java b/java/src/com/android/inputmethod/compat/AbstractCompatWrapper.java
new file mode 100644
index 000000000..65949357f
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/AbstractCompatWrapper.java
@@ -0,0 +1,39 @@
+/*
+ * 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.compat;
+
+import android.util.Log;
+
+public abstract class AbstractCompatWrapper {
+ private static final String TAG = AbstractCompatWrapper.class.getSimpleName();
+ protected final Object mObj;
+
+ public AbstractCompatWrapper(Object obj) {
+ if (obj == null) {
+ Log.e(TAG, "Invalid input to AbstructCompatWrapper");
+ }
+ mObj = obj;
+ }
+
+ public Object getOriginalObject() {
+ return mObj;
+ }
+
+ public boolean hasOriginalObject() {
+ return mObj != null;
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java b/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java
new file mode 100644
index 000000000..50057727a
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java
@@ -0,0 +1,39 @@
+/*
+ * 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.compat;
+
+import android.view.accessibility.AccessibilityEvent;
+
+import java.lang.reflect.Field;
+
+public class AccessibilityEventCompatUtils {
+ public static final int TYPE_VIEW_HOVER_ENTER = 0x80;
+ public static final int TYPE_VIEW_HOVER_EXIT = 0x100;
+
+ private static final Field FIELD_TYPE_VIEW_HOVER_ENTER = CompatUtils.getField(
+ AccessibilityEvent.class, "TYPE_VIEW_HOVER_ENTER");
+ private static final Field FIELD_TYPE_VIEW_HOVER_EXIT = CompatUtils.getField(
+ AccessibilityEvent.class, "TYPE_VIEW_HOVER_EXIT");
+ private static final Integer OBJ_TYPE_VIEW_HOVER_ENTER = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_TYPE_VIEW_HOVER_ENTER);
+ private static final Integer OBJ_TYPE_VIEW_HOVER_EXIT = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_TYPE_VIEW_HOVER_EXIT);
+
+ public static boolean supportsTouchExploration() {
+ return OBJ_TYPE_VIEW_HOVER_ENTER != null && OBJ_TYPE_VIEW_HOVER_EXIT != null;
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java
new file mode 100644
index 000000000..4db1c7a24
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java
@@ -0,0 +1,42 @@
+/*
+ * 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.compat;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.view.accessibility.AccessibilityManager;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+public class AccessibilityManagerCompatWrapper {
+ private static final Method METHOD_getEnabledAccessibilityServiceList = CompatUtils.getMethod(
+ AccessibilityManager.class, "getEnabledAccessibilityServiceList", int.class);
+
+ private final AccessibilityManager mManager;
+
+ public AccessibilityManagerCompatWrapper(AccessibilityManager manager) {
+ mManager = manager;
+ }
+
+ @SuppressWarnings("unchecked")
+ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType) {
+ return (List<AccessibilityServiceInfo>) CompatUtils.invoke(mManager,
+ Collections.<AccessibilityServiceInfo>emptyList(),
+ METHOD_getEnabledAccessibilityServiceList, feedbackType);
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/ArraysCompatUtils.java b/java/src/com/android/inputmethod/compat/ArraysCompatUtils.java
new file mode 100644
index 000000000..f6afbcfe2
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/ArraysCompatUtils.java
@@ -0,0 +1,50 @@
+/*
+ * 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.compat;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+public class ArraysCompatUtils {
+ private static final Method METHOD_Arrays_binarySearch = CompatUtils
+ .getMethod(Arrays.class, "binarySearch", int[].class, int.class, int.class, int.class);
+
+ public static int binarySearch(int[] array, int startIndex, int endIndex, int value) {
+ if (METHOD_Arrays_binarySearch != null) {
+ final Object index = CompatUtils.invoke(null, 0, METHOD_Arrays_binarySearch,
+ array, startIndex, endIndex, value);
+ return (Integer)index;
+ } else {
+ return compatBinarySearch(array, startIndex, endIndex, value);
+ }
+ }
+
+ /* package */ static int compatBinarySearch(int[] array, int startIndex, int endIndex,
+ int value) {
+ if (startIndex > endIndex) throw new IllegalArgumentException();
+ if (startIndex < 0 || endIndex > array.length) throw new ArrayIndexOutOfBoundsException();
+
+ final int work[] = new int[endIndex - startIndex];
+ System.arraycopy(array, startIndex, work, 0, work.length);
+ final int index = Arrays.binarySearch(work, value);
+ if (index >= 0) {
+ return index + startIndex;
+ } else {
+ return ~(~index + startIndex);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/CompatUtils.java b/java/src/com/android/inputmethod/compat/CompatUtils.java
new file mode 100644
index 000000000..b42633cd9
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/CompatUtils.java
@@ -0,0 +1,156 @@
+/*
+ * 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.compat;
+
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CompatUtils {
+ private static final String TAG = CompatUtils.class.getSimpleName();
+ private static final String EXTRA_INPUT_METHOD_ID = "input_method_id";
+ // TODO: Can these be constants instead of literal String constants?
+ private static final String INPUT_METHOD_SUBTYPE_SETTINGS =
+ "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS";
+ private static final String INPUT_LANGUAGE_SELECTION =
+ "com.android.inputmethod.latin.INPUT_LANGUAGE_SELECTION";
+
+ public static Intent getInputLanguageSelectionIntent(String inputMethodId,
+ int flagsForSubtypeSettings) {
+ final String action;
+ Intent intent;
+ if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED
+ /* android.os.Build.VERSION_CODES.HONEYCOMB */
+ && android.os.Build.VERSION.SDK_INT >= 11) {
+ // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS
+ action = INPUT_METHOD_SUBTYPE_SETTINGS;
+ intent = new Intent(action);
+ if (!TextUtils.isEmpty(inputMethodId)) {
+ intent.putExtra(EXTRA_INPUT_METHOD_ID, inputMethodId);
+ }
+ if (flagsForSubtypeSettings > 0) {
+ intent.setFlags(flagsForSubtypeSettings);
+ }
+ } else {
+ action = INPUT_LANGUAGE_SELECTION;
+ intent = new Intent(action);
+ }
+ return intent;
+ }
+
+ public static Class<?> getClass(String className) {
+ try {
+ return Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ public static Method getMethod(Class<?> targetClass, String name,
+ Class<?>... parameterTypes) {
+ if (targetClass == null || TextUtils.isEmpty(name)) return null;
+ try {
+ return targetClass.getMethod(name, parameterTypes);
+ } catch (SecurityException e) {
+ // ignore
+ } catch (NoSuchMethodException e) {
+ // ignore
+ }
+ return null;
+ }
+
+ public static Field getField(Class<?> targetClass, String name) {
+ if (targetClass == null || TextUtils.isEmpty(name)) return null;
+ try {
+ return targetClass.getField(name);
+ } catch (SecurityException e) {
+ // ignore
+ } catch (NoSuchFieldException e) {
+ // ignore
+ }
+ return null;
+ }
+
+ public static Constructor<?> getConstructor(Class<?> targetClass, Class<?> ... types) {
+ if (targetClass == null || types == null) return null;
+ try {
+ return targetClass.getConstructor(types);
+ } catch (SecurityException e) {
+ // ignore
+ } catch (NoSuchMethodException e) {
+ // ignore
+ }
+ return null;
+ }
+
+ public static Object newInstance(Constructor<?> constructor, Object ... args) {
+ if (constructor == null) return null;
+ try {
+ return constructor.newInstance(args);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in newInstance: " + e.getClass().getSimpleName());
+ }
+ return null;
+ }
+
+ public static Object invoke(
+ Object receiver, Object defaultValue, Method method, Object... args) {
+ if (method == null) return defaultValue;
+ try {
+ return method.invoke(receiver, args);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in invoke: " + e.getClass().getSimpleName());
+ }
+ return defaultValue;
+ }
+
+ public static Object getFieldValue(Object receiver, Object defaultValue, Field field) {
+ if (field == null) return defaultValue;
+ try {
+ return field.get(receiver);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in getFieldValue: " + e.getClass().getSimpleName());
+ }
+ return defaultValue;
+ }
+
+ public static void setFieldValue(Object receiver, Field field, Object value) {
+ if (field == null) return;
+ try {
+ field.set(receiver, value);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in setFieldValue: " + e.getClass().getSimpleName());
+ }
+ }
+
+ public static List<InputMethodSubtypeCompatWrapper> copyInputMethodSubtypeListToWrapper(
+ Object listObject) {
+ if (!(listObject instanceof List<?>)) return null;
+ final List<InputMethodSubtypeCompatWrapper> subtypes =
+ new ArrayList<InputMethodSubtypeCompatWrapper>();
+ for (Object o: (List<?>)listObject) {
+ subtypes.add(new InputMethodSubtypeCompatWrapper(o));
+ }
+ return subtypes;
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
new file mode 100644
index 000000000..bcdcef7dc
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
@@ -0,0 +1,102 @@
+/*
+ * 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.compat;
+
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import java.lang.reflect.Field;
+
+public class EditorInfoCompatUtils {
+ private static final Field FIELD_IME_FLAG_NAVIGATE_NEXT = CompatUtils.getField(
+ EditorInfo.class, "IME_FLAG_NAVIGATE_NEXT");
+ private static final Field FIELD_IME_FLAG_NAVIGATE_PREVIOUS = CompatUtils.getField(
+ EditorInfo.class, "IME_FLAG_NAVIGATE_PREVIOUS");
+ private static final Field FIELD_IME_ACTION_PREVIOUS = CompatUtils.getField(
+ EditorInfo.class, "IME_ACTION_PREVIOUS");
+ private static final Integer OBJ_IME_FLAG_NAVIGATE_NEXT = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_IME_FLAG_NAVIGATE_NEXT);
+ private static final Integer OBJ_IME_FLAG_NAVIGATE_PREVIOUS = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_IME_FLAG_NAVIGATE_PREVIOUS);
+ private static final Integer OBJ_IME_ACTION_PREVIOUS = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_IME_ACTION_PREVIOUS);
+
+ public static boolean hasFlagNavigateNext(int imeOptions) {
+ if (OBJ_IME_FLAG_NAVIGATE_NEXT == null)
+ return false;
+ return (imeOptions & OBJ_IME_FLAG_NAVIGATE_NEXT) != 0;
+ }
+
+ public static boolean hasFlagNavigatePrevious(int imeOptions) {
+ if (OBJ_IME_FLAG_NAVIGATE_PREVIOUS == null)
+ return false;
+ return (imeOptions & OBJ_IME_FLAG_NAVIGATE_PREVIOUS) != 0;
+ }
+
+ public static void performEditorActionNext(InputConnection ic) {
+ ic.performEditorAction(EditorInfo.IME_ACTION_NEXT);
+ }
+
+ public static void performEditorActionPrevious(InputConnection ic) {
+ if (OBJ_IME_ACTION_PREVIOUS == null)
+ return;
+ ic.performEditorAction(OBJ_IME_ACTION_PREVIOUS);
+ }
+
+ public static String imeOptionsName(int imeOptions) {
+ if (imeOptions == -1)
+ return null;
+ final int actionId = imeOptions & EditorInfo.IME_MASK_ACTION;
+ final String action;
+ switch (actionId) {
+ case EditorInfo.IME_ACTION_UNSPECIFIED:
+ action = "actionUnspecified";
+ break;
+ case EditorInfo.IME_ACTION_NONE:
+ action = "actionNone";
+ break;
+ case EditorInfo.IME_ACTION_GO:
+ action = "actionGo";
+ break;
+ case EditorInfo.IME_ACTION_SEARCH:
+ action = "actionSearch";
+ break;
+ case EditorInfo.IME_ACTION_SEND:
+ action = "actionSend";
+ break;
+ case EditorInfo.IME_ACTION_NEXT:
+ action = "actionNext";
+ break;
+ case EditorInfo.IME_ACTION_DONE:
+ action = "actionDone";
+ break;
+ default: {
+ if (OBJ_IME_ACTION_PREVIOUS != null && actionId == OBJ_IME_ACTION_PREVIOUS) {
+ action = "actionPrevious";
+ } else {
+ action = "actionUnknown(" + actionId + ")";
+ }
+ break;
+ }
+ }
+ if ((imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
+ return "flagNoEnterAction|" + action;
+ } else {
+ return action;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/FrameLayoutCompatUtils.java b/java/src/com/android/inputmethod/compat/FrameLayoutCompatUtils.java
new file mode 100644
index 000000000..523bf7d0e
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/FrameLayoutCompatUtils.java
@@ -0,0 +1,63 @@
+/*
+ * 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.compat;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+
+public class FrameLayoutCompatUtils {
+ private static final boolean NEEDS_FRAME_LAYOUT_HACK = (
+ android.os.Build.VERSION.SDK_INT < 11 /* Honeycomb */);
+
+ public static ViewGroup getPlacer(ViewGroup container) {
+ if (NEEDS_FRAME_LAYOUT_HACK) {
+ // Insert RelativeLayout to be able to setMargin because pre-Honeycomb FrameLayout
+ // could not handle setMargin properly.
+ final ViewGroup placer = new RelativeLayout(container.getContext());
+ container.addView(placer);
+ return placer;
+ } else {
+ return container;
+ }
+ }
+
+ public static MarginLayoutParams newLayoutParam(ViewGroup placer, int width, int height) {
+ if (placer instanceof FrameLayout) {
+ return new FrameLayout.LayoutParams(width, height);
+ } else if (placer instanceof RelativeLayout) {
+ return new RelativeLayout.LayoutParams(width, height);
+ } else if (placer == null) {
+ throw new NullPointerException("placer is null");
+ } else {
+ throw new IllegalArgumentException("placer is neither FrameLayout nor RelativeLayout: "
+ + placer.getClass().getName());
+ }
+ }
+
+ public static void placeViewAt(View view, int x, int y, int w, int h) {
+ final ViewGroup.LayoutParams lp = view.getLayoutParams();
+ if (lp instanceof MarginLayoutParams) {
+ final MarginLayoutParams marginLayoutParams = (MarginLayoutParams)lp;
+ marginLayoutParams.width = w;
+ marginLayoutParams.height = h;
+ marginLayoutParams.setMargins(x, y, 0, 0);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/InputConnectionCompatUtils.java b/java/src/com/android/inputmethod/compat/InputConnectionCompatUtils.java
new file mode 100644
index 000000000..7d00b6007
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputConnectionCompatUtils.java
@@ -0,0 +1,80 @@
+/*
+ * 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.compat;
+
+import com.android.inputmethod.latin.EditingUtils.SelectedWord;
+
+import android.view.inputmethod.InputConnection;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+public class InputConnectionCompatUtils {
+ private static final Class<?> CLASS_CorrectionInfo = CompatUtils
+ .getClass("android.view.inputmethod.CorrectionInfo");
+ private static final Class<?>[] INPUT_TYPE_CorrectionInfo = new Class<?>[] { int.class,
+ CharSequence.class, CharSequence.class };
+ private static final Constructor<?> CONSTRUCTOR_CorrectionInfo = CompatUtils
+ .getConstructor(CLASS_CorrectionInfo, INPUT_TYPE_CorrectionInfo);
+ private static final Method METHOD_InputConnection_commitCorrection = CompatUtils
+ .getMethod(InputConnection.class, "commitCorrection", CLASS_CorrectionInfo);
+ private static final Method METHOD_getSelectedText = CompatUtils
+ .getMethod(InputConnection.class, "getSelectedText", int.class);
+ private static final Method METHOD_setComposingRegion = CompatUtils
+ .getMethod(InputConnection.class, "setComposingRegion", int.class, int.class);
+ public static final boolean RECORRECTION_SUPPORTED;
+
+ static {
+ RECORRECTION_SUPPORTED = METHOD_getSelectedText != null
+ && METHOD_setComposingRegion != null;
+ }
+
+ public static void commitCorrection(InputConnection ic, int offset, CharSequence oldText,
+ CharSequence newText) {
+ if (ic == null || CONSTRUCTOR_CorrectionInfo == null
+ || METHOD_InputConnection_commitCorrection == null) {
+ return;
+ }
+ Object[] args = { offset, oldText, newText };
+ Object correctionInfo = CompatUtils.newInstance(CONSTRUCTOR_CorrectionInfo, args);
+ if (correctionInfo != null) {
+ CompatUtils.invoke(ic, null, METHOD_InputConnection_commitCorrection,
+ correctionInfo);
+ }
+ }
+
+
+ /**
+ * Returns the selected text between the selStart and selEnd positions.
+ */
+ public static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) {
+ // Use reflection, for backward compatibility
+ return (CharSequence) CompatUtils.invoke(
+ ic, null, METHOD_getSelectedText, 0);
+ }
+
+ /**
+ * Tries to set the text into composition mode if there is support for it in the framework.
+ */
+ public static void underlineWord(InputConnection ic, SelectedWord word) {
+ // Use reflection, for backward compatibility
+ // If method not found, there's nothing we can do. It still works but just wont underline
+ // the word.
+ CompatUtils.invoke(
+ ic, null, METHOD_setComposingRegion, word.mStart, word.mEnd);
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/InputMethodInfoCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodInfoCompatWrapper.java
new file mode 100644
index 000000000..8e22bbc79
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputMethodInfoCompatWrapper.java
@@ -0,0 +1,59 @@
+/*
+ * 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.compat;
+
+import android.content.pm.ServiceInfo;
+import android.view.inputmethod.InputMethodInfo;
+
+import java.lang.reflect.Method;
+
+public class InputMethodInfoCompatWrapper {
+ private final InputMethodInfo mImi;
+ private static final Method METHOD_getSubtypeAt = CompatUtils.getMethod(
+ InputMethodInfo.class, "getSubtypeAt", int.class);
+ private static final Method METHOD_getSubtypeCount = CompatUtils.getMethod(
+ InputMethodInfo.class, "getSubtypeCount");
+
+ public InputMethodInfoCompatWrapper(InputMethodInfo imi) {
+ mImi = imi;
+ }
+
+ public InputMethodInfo getInputMethodInfo() {
+ return mImi;
+ }
+
+ public String getId() {
+ return mImi.getId();
+ }
+
+ public String getPackageName() {
+ return mImi.getPackageName();
+ }
+
+ public ServiceInfo getServiceInfo() {
+ return mImi.getServiceInfo();
+ }
+
+ public int getSubtypeCount() {
+ return (Integer) CompatUtils.invoke(mImi, 0, METHOD_getSubtypeCount);
+ }
+
+ public InputMethodSubtypeCompatWrapper getSubtypeAt(int index) {
+ return new InputMethodSubtypeCompatWrapper(CompatUtils.invoke(mImi, null,
+ METHOD_getSubtypeAt, index));
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java
new file mode 100644
index 000000000..1cc13f249
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java
@@ -0,0 +1,227 @@
+/*
+ * 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.compat;
+
+import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.Utils;
+
+import android.content.Context;
+import android.os.IBinder;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+// TODO: Override this class with the concrete implementation if we need to take care of the
+// performance.
+public class InputMethodManagerCompatWrapper {
+ private static final String TAG = InputMethodManagerCompatWrapper.class.getSimpleName();
+ private static final Method METHOD_getCurrentInputMethodSubtype =
+ CompatUtils.getMethod(InputMethodManager.class, "getCurrentInputMethodSubtype");
+ private static final Method METHOD_getEnabledInputMethodSubtypeList =
+ CompatUtils.getMethod(InputMethodManager.class, "getEnabledInputMethodSubtypeList",
+ InputMethodInfo.class, boolean.class);
+ private static final Method METHOD_getShortcutInputMethodsAndSubtypes =
+ CompatUtils.getMethod(InputMethodManager.class, "getShortcutInputMethodsAndSubtypes");
+ private static final Method METHOD_setInputMethodAndSubtype =
+ CompatUtils.getMethod(
+ InputMethodManager.class, "setInputMethodAndSubtype", IBinder.class,
+ String.class, InputMethodSubtypeCompatWrapper.CLASS_InputMethodSubtype);
+ private static final Method METHOD_switchToLastInputMethod = CompatUtils.getMethod(
+ InputMethodManager.class, "switchToLastInputMethod", IBinder.class);
+
+ private static final InputMethodManagerCompatWrapper sInstance =
+ new InputMethodManagerCompatWrapper();
+
+ public static final boolean SUBTYPE_SUPPORTED;
+
+ static {
+ // This static initializer guarantees that METHOD_getShortcutInputMethodsAndSubtypes is
+ // already instantiated.
+ SUBTYPE_SUPPORTED = METHOD_getShortcutInputMethodsAndSubtypes != null;
+ }
+
+ // For the compatibility, IMM will create dummy subtypes if subtypes are not found.
+ // This is required to be false if the current behavior is broken. For now, it's ok to be true.
+ public static final boolean FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES =
+ !InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED;
+ private static final String VOICE_MODE = "voice";
+ private static final String KEYBOARD_MODE = "keyboard";
+
+ private InputMethodManager mImm;
+ private LanguageSwitcherProxy mLanguageSwitcherProxy;
+ private String mLatinImePackageName;
+
+ private InputMethodManagerCompatWrapper() {
+ }
+
+ public static InputMethodManagerCompatWrapper getInstance(Context context) {
+ if (sInstance.mImm == null) {
+ sInstance.init(context);
+ }
+ return sInstance;
+ }
+
+ private synchronized void init(Context context) {
+ mImm = (InputMethodManager) context.getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+ if (context instanceof LatinIME) {
+ mLatinImePackageName = context.getPackageName();
+ }
+ mLanguageSwitcherProxy = LanguageSwitcherProxy.getInstance();
+ }
+
+ public InputMethodSubtypeCompatWrapper getCurrentInputMethodSubtype() {
+ if (!SUBTYPE_SUPPORTED) {
+ return new InputMethodSubtypeCompatWrapper(
+ 0, 0, mLanguageSwitcherProxy.getInputLocale().toString(), KEYBOARD_MODE, "");
+ }
+ Object o = CompatUtils.invoke(mImm, null, METHOD_getCurrentInputMethodSubtype);
+ return new InputMethodSubtypeCompatWrapper(o);
+ }
+
+ public List<InputMethodSubtypeCompatWrapper> getEnabledInputMethodSubtypeList(
+ InputMethodInfoCompatWrapper imi, boolean allowsImplicitlySelectedSubtypes) {
+ if (!SUBTYPE_SUPPORTED) {
+ String[] languages = mLanguageSwitcherProxy.getEnabledLanguages(
+ allowsImplicitlySelectedSubtypes);
+ List<InputMethodSubtypeCompatWrapper> subtypeList =
+ new ArrayList<InputMethodSubtypeCompatWrapper>();
+ for (String lang: languages) {
+ subtypeList.add(new InputMethodSubtypeCompatWrapper(0, 0, lang, KEYBOARD_MODE, ""));
+ }
+ return subtypeList;
+ }
+ Object retval = CompatUtils.invoke(mImm, null, METHOD_getEnabledInputMethodSubtypeList,
+ (imi != null ? imi.getInputMethodInfo() : null), allowsImplicitlySelectedSubtypes);
+ if (retval == null || !(retval instanceof List<?>) || ((List<?>)retval).isEmpty()) {
+ if (!FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES) {
+ // Returns an empty list
+ return Collections.emptyList();
+ }
+ // Creates dummy subtypes
+ @SuppressWarnings("unused")
+ List<InputMethodSubtypeCompatWrapper> subtypeList =
+ new ArrayList<InputMethodSubtypeCompatWrapper>();
+ InputMethodSubtypeCompatWrapper keyboardSubtype = getLastResortSubtype(KEYBOARD_MODE);
+ InputMethodSubtypeCompatWrapper voiceSubtype = getLastResortSubtype(VOICE_MODE);
+ if (keyboardSubtype != null) {
+ subtypeList.add(keyboardSubtype);
+ }
+ if (voiceSubtype != null) {
+ subtypeList.add(voiceSubtype);
+ }
+ return subtypeList;
+ }
+ return CompatUtils.copyInputMethodSubtypeListToWrapper(retval);
+ }
+
+ private InputMethodInfoCompatWrapper getLatinImeInputMethodInfo() {
+ if (TextUtils.isEmpty(mLatinImePackageName))
+ return null;
+ return Utils.getInputMethodInfo(this, mLatinImePackageName);
+ }
+
+ @SuppressWarnings("unused")
+ private InputMethodSubtypeCompatWrapper getLastResortSubtype(String mode) {
+ if (VOICE_MODE.equals(mode) && !FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES)
+ return null;
+ Locale inputLocale = SubtypeSwitcher.getInstance().getInputLocale();
+ if (inputLocale == null)
+ return null;
+ return new InputMethodSubtypeCompatWrapper(0, 0, inputLocale.toString(), mode, "");
+ }
+
+ public Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>
+ getShortcutInputMethodsAndSubtypes() {
+ Object retval = CompatUtils.invoke(mImm, null, METHOD_getShortcutInputMethodsAndSubtypes);
+ if (retval == null || !(retval instanceof Map<?, ?>) || ((Map<?, ?>)retval).isEmpty()) {
+ if (!FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES) {
+ // Returns an empty map
+ return Collections.emptyMap();
+ }
+ // Creates dummy subtypes
+ @SuppressWarnings("unused")
+ InputMethodInfoCompatWrapper imi = getLatinImeInputMethodInfo();
+ InputMethodSubtypeCompatWrapper voiceSubtype = getLastResortSubtype(VOICE_MODE);
+ if (imi != null && voiceSubtype != null) {
+ Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>
+ shortcutMap =
+ new HashMap<InputMethodInfoCompatWrapper,
+ List<InputMethodSubtypeCompatWrapper>>();
+ List<InputMethodSubtypeCompatWrapper> subtypeList =
+ new ArrayList<InputMethodSubtypeCompatWrapper>();
+ subtypeList.add(voiceSubtype);
+ shortcutMap.put(imi, subtypeList);
+ return shortcutMap;
+ } else {
+ return Collections.emptyMap();
+ }
+ }
+ Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcutMap =
+ new HashMap<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>();
+ final Map<?, ?> retvalMap = (Map<?, ?>)retval;
+ for (Object key : retvalMap.keySet()) {
+ if (!(key instanceof InputMethodInfo)) {
+ Log.e(TAG, "Class type error.");
+ return null;
+ }
+ shortcutMap.put(new InputMethodInfoCompatWrapper((InputMethodInfo)key),
+ CompatUtils.copyInputMethodSubtypeListToWrapper(retvalMap.get(key)));
+ }
+ return shortcutMap;
+ }
+
+ public void setInputMethodAndSubtype(
+ IBinder token, String id, InputMethodSubtypeCompatWrapper subtype) {
+ if (subtype != null && subtype.hasOriginalObject()) {
+ CompatUtils.invoke(mImm, null, METHOD_setInputMethodAndSubtype,
+ token, id, subtype.getOriginalObject());
+ }
+ }
+
+ public boolean switchToLastInputMethod(IBinder token) {
+ if (SubtypeSwitcher.getInstance().isDummyVoiceMode()) {
+ return true;
+ }
+ return (Boolean)CompatUtils.invoke(mImm, false, METHOD_switchToLastInputMethod, token);
+ }
+
+ public List<InputMethodInfoCompatWrapper> getEnabledInputMethodList() {
+ if (mImm == null) return null;
+ List<InputMethodInfoCompatWrapper> imis = new ArrayList<InputMethodInfoCompatWrapper>();
+ for (InputMethodInfo imi : mImm.getEnabledInputMethodList()) {
+ imis.add(new InputMethodInfoCompatWrapper(imi));
+ }
+ return imis;
+ }
+
+ public void showInputMethodPicker() {
+ if (mImm == null) return;
+ mImm.showInputMethodPicker();
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java
new file mode 100644
index 000000000..7d8c745c3
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.compat;
+
+import android.inputmethodservice.InputMethodService;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+
+public class InputMethodServiceCompatWrapper extends InputMethodService {
+ // CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED needs to be false if the API level is 10
+ // or previous. Note that InputMethodSubtype was added in the API level 11.
+ // For the API level 11 or later, LatinIME should override onCurrentInputMethodSubtypeChanged().
+ // For the API level 10 or previous, we handle the "subtype changed" events by ourselves
+ // without having support from framework -- onCurrentInputMethodSubtypeChanged().
+ public static final boolean CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED = true;
+
+ private InputMethodManagerCompatWrapper mImm;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mImm = InputMethodManagerCompatWrapper.getInstance(this);
+ }
+
+ // When the API level is 10 or previous, notifyOnCurrentInputMethodSubtypeChanged should
+ // handle the event the current subtype was changed. LatinIME calls
+ // notifyOnCurrentInputMethodSubtypeChanged every time LatinIME
+ // changes the current subtype.
+ // This call is required to let LatinIME itself know a subtype changed
+ // event when the API level is 10 or previous.
+ @SuppressWarnings("unused")
+ public void notifyOnCurrentInputMethodSubtypeChanged(InputMethodSubtypeCompatWrapper subtype) {
+ // Do nothing when the API level is 11 or later
+ // and FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES is not true
+ if (CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED && !InputMethodManagerCompatWrapper.
+ FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES) {
+ return;
+ }
+ if (subtype == null) {
+ subtype = mImm.getCurrentInputMethodSubtype();
+ }
+ if (subtype != null) {
+ if (!InputMethodManagerCompatWrapper.FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES
+ && !subtype.isDummy()) return;
+ if (!InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) {
+ LanguageSwitcherProxy.getInstance().setLocale(subtype.getLocale());
+ }
+ SubtypeSwitcher.getInstance().updateSubtype(subtype);
+ }
+ }
+
+ //////////////////////////////////////
+ // Functions using API v11 or later //
+ //////////////////////////////////////
+ @Override
+ public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
+ // Do nothing when the API level is 10 or previous
+ if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) return;
+ SubtypeSwitcher.getInstance().updateSubtype(
+ new InputMethodSubtypeCompatWrapper(subtype));
+ }
+
+ protected static void setTouchableRegionCompat(InputMethodService.Insets outInsets,
+ int x, int y, int width, int height) {
+ outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
+ outInsets.touchableRegion.set(x, y, width, height);
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatWrapper.java
new file mode 100644
index 000000000..667d86c42
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatWrapper.java
@@ -0,0 +1,161 @@
+/*
+ * 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.compat;
+
+import com.android.inputmethod.latin.LatinImeLogger;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+// TODO: Override this class with the concrete implementation if we need to take care of the
+// performance.
+public final class InputMethodSubtypeCompatWrapper extends AbstractCompatWrapper {
+ private static final boolean DBG = LatinImeLogger.sDBG;
+ private static final String TAG = InputMethodSubtypeCompatWrapper.class.getSimpleName();
+ private static final String DEFAULT_LOCALE = "en_US";
+ private static final String DEFAULT_MODE = "keyboard";
+
+ public static final Class<?> CLASS_InputMethodSubtype =
+ CompatUtils.getClass("android.view.inputmethod.InputMethodSubtype");
+ private static final Method METHOD_getNameResId =
+ CompatUtils.getMethod(CLASS_InputMethodSubtype, "getNameResId");
+ private static final Method METHOD_getIconResId =
+ CompatUtils.getMethod(CLASS_InputMethodSubtype, "getIconResId");
+ private static final Method METHOD_getLocale =
+ CompatUtils.getMethod(CLASS_InputMethodSubtype, "getLocale");
+ private static final Method METHOD_getMode =
+ CompatUtils.getMethod(CLASS_InputMethodSubtype, "getMode");
+ private static final Method METHOD_getExtraValue =
+ CompatUtils.getMethod(CLASS_InputMethodSubtype, "getExtraValue");
+ private static final Method METHOD_containsExtraValueKey =
+ CompatUtils.getMethod(CLASS_InputMethodSubtype, "containsExtraValueKey", String.class);
+ private static final Method METHOD_getExtraValueOf =
+ CompatUtils.getMethod(CLASS_InputMethodSubtype, "getExtraValueOf", String.class);
+ private static final Method METHOD_isAuxiliary =
+ CompatUtils.getMethod(CLASS_InputMethodSubtype, "isAuxiliary");
+
+ private final int mDummyNameResId;
+ private final int mDummyIconResId;
+ private final String mDummyLocale;
+ private final String mDummyMode;
+ private final String mDummyExtraValues;
+
+ public InputMethodSubtypeCompatWrapper(Object subtype) {
+ super((CLASS_InputMethodSubtype != null && CLASS_InputMethodSubtype.isInstance(subtype))
+ ? subtype : null);
+ mDummyNameResId = 0;
+ mDummyIconResId = 0;
+ mDummyLocale = DEFAULT_LOCALE;
+ mDummyMode = DEFAULT_MODE;
+ mDummyExtraValues = "";
+ }
+
+ // Constructor for creating a dummy subtype.
+ public InputMethodSubtypeCompatWrapper(int nameResId, int iconResId, String locale,
+ String mode, String extraValues) {
+ super(null);
+ if (DBG) {
+ Log.d(TAG, "CreateInputMethodSubtypeCompatWrapper");
+ }
+ mDummyNameResId = nameResId;
+ mDummyIconResId = iconResId;
+ mDummyLocale = locale != null ? locale : "";
+ mDummyMode = mode != null ? mode : "";
+ mDummyExtraValues = extraValues != null ? extraValues : "";
+ }
+
+ public int getNameResId() {
+ if (mObj == null) return mDummyNameResId;
+ return (Integer)CompatUtils.invoke(mObj, 0, METHOD_getNameResId);
+ }
+
+ public int getIconResId() {
+ if (mObj == null) return mDummyIconResId;
+ return (Integer)CompatUtils.invoke(mObj, 0, METHOD_getIconResId);
+ }
+
+ public String getLocale() {
+ if (mObj == null) return mDummyLocale;
+ final String s = (String)CompatUtils.invoke(mObj, null, METHOD_getLocale);
+ if (TextUtils.isEmpty(s)) return DEFAULT_LOCALE;
+ return s;
+ }
+
+ public String getMode() {
+ if (mObj == null) return mDummyMode;
+ String s = (String)CompatUtils.invoke(mObj, null, METHOD_getMode);
+ if (TextUtils.isEmpty(s)) return DEFAULT_MODE;
+ return s;
+ }
+
+ public String getExtraValue() {
+ if (mObj == null) return mDummyExtraValues;
+ return (String)CompatUtils.invoke(mObj, null, METHOD_getExtraValue);
+ }
+
+ public boolean containsExtraValueKey(String key) {
+ return (Boolean)CompatUtils.invoke(mObj, false, METHOD_containsExtraValueKey, key);
+ }
+
+ public String getExtraValueOf(String key) {
+ return (String)CompatUtils.invoke(mObj, null, METHOD_getExtraValueOf, key);
+ }
+
+ public boolean isAuxiliary() {
+ return (Boolean)CompatUtils.invoke(mObj, false, METHOD_isAuxiliary);
+ }
+
+ public boolean isDummy() {
+ return !hasOriginalObject();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof InputMethodSubtypeCompatWrapper) {
+ InputMethodSubtypeCompatWrapper subtype = (InputMethodSubtypeCompatWrapper)o;
+ if (mObj == null) {
+ // easy check of dummy subtypes
+ return (mDummyNameResId == subtype.mDummyNameResId
+ && mDummyIconResId == subtype.mDummyIconResId
+ && mDummyLocale.equals(subtype.mDummyLocale)
+ && mDummyMode.equals(subtype.mDummyMode)
+ && mDummyExtraValues.equals(subtype.mDummyExtraValues));
+ }
+ return mObj.equals(subtype.getOriginalObject());
+ } else {
+ return mObj.equals(o);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ if (mObj == null) {
+ return hashCodeInternal(mDummyNameResId, mDummyIconResId, mDummyLocale,
+ mDummyMode, mDummyExtraValues);
+ }
+ return mObj.hashCode();
+ }
+
+ private static int hashCodeInternal(int nameResId, int iconResId, String locale,
+ String mode, String extraValue) {
+ return Arrays
+ .hashCode(new Object[] { nameResId, iconResId, locale, mode, extraValue });
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/InputTypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputTypeCompatUtils.java
new file mode 100644
index 000000000..6c2f0f799
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputTypeCompatUtils.java
@@ -0,0 +1,118 @@
+/*
+ * 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.compat;
+
+import android.text.InputType;
+
+import java.lang.reflect.Field;
+
+public class InputTypeCompatUtils {
+ private static final Field FIELD_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS =
+ CompatUtils.getField(InputType.class, "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS");
+ private static final Field FIELD_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD = CompatUtils
+ .getField(InputType.class, "TYPE_TEXT_VARIATION_WEB_PASSWORD");
+ private static final Field FIELD_InputType_TYPE_NUMBER_VARIATION_PASSWORD = CompatUtils
+ .getField(InputType.class, "TYPE_NUMBER_VARIATION_PASSWORD");
+ private static final Integer OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS =
+ (Integer) CompatUtils.getFieldValue(null, null,
+ FIELD_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
+ private static final Integer OBJ_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD =
+ (Integer) CompatUtils.getFieldValue(null, null,
+ FIELD_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD);
+ private static final Integer OBJ_InputType_TYPE_NUMBER_VARIATION_PASSWORD =
+ (Integer) CompatUtils.getFieldValue(null, null,
+ FIELD_InputType_TYPE_NUMBER_VARIATION_PASSWORD);
+ private static final int WEB_TEXT_PASSWORD_INPUT_TYPE;
+ private static final int WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE;
+ private static final int NUMBER_PASSWORD_INPUT_TYPE;
+ private static final int TEXT_PASSWORD_INPUT_TYPE =
+ InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ private static final int TEXT_VISIBLE_PASSWORD_INPUT_TYPE =
+ InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
+
+ static {
+ WEB_TEXT_PASSWORD_INPUT_TYPE =
+ OBJ_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD != null
+ ? InputType.TYPE_CLASS_TEXT | OBJ_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD
+ : 0;
+ WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE =
+ OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != null
+ ? InputType.TYPE_CLASS_TEXT
+ | OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
+ : 0;
+ NUMBER_PASSWORD_INPUT_TYPE =
+ OBJ_InputType_TYPE_NUMBER_VARIATION_PASSWORD != null
+ ? InputType.TYPE_CLASS_NUMBER | OBJ_InputType_TYPE_NUMBER_VARIATION_PASSWORD
+ : 0;
+ }
+
+ private static boolean isWebEditTextInputType(int inputType) {
+ return inputType == (InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+ }
+
+ private static boolean isWebPasswordInputType(int inputType) {
+ return WEB_TEXT_PASSWORD_INPUT_TYPE != 0
+ && inputType == WEB_TEXT_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isWebEmailAddressInputType(int inputType) {
+ return WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE != 0
+ && inputType == WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE;
+ }
+
+ private static boolean isNumberPasswordInputType(int inputType) {
+ return NUMBER_PASSWORD_INPUT_TYPE != 0
+ && inputType == NUMBER_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isTextPasswordInputType(int inputType) {
+ return inputType == TEXT_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isWebEmailAddressVariation(int variation) {
+ return OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != null
+ && variation == OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
+ }
+
+ public static boolean isEmailVariation(int variation) {
+ return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || isWebEmailAddressVariation(variation);
+ }
+
+ public static boolean isWebInputType(int inputType) {
+ final int maskedInputType =
+ inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION);
+ return isWebEditTextInputType(maskedInputType) || isWebPasswordInputType(maskedInputType)
+ || isWebEmailAddressInputType(maskedInputType);
+ }
+
+ // Please refer to TextView.isPasswordInputType
+ public static boolean isPasswordInputType(int inputType) {
+ final int maskedInputType =
+ inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION);
+ return isTextPasswordInputType(maskedInputType) || isWebPasswordInputType(maskedInputType)
+ || isNumberPasswordInputType(maskedInputType);
+ }
+
+ // Please refer to TextView.isVisiblePasswordInputType
+ public static boolean isVisiblePasswordInputType(int inputType) {
+ final int maskedInputType =
+ inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION);
+ return maskedInputType == TEXT_VISIBLE_PASSWORD_INPUT_TYPE;
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/LinearLayoutCompatUtils.java b/java/src/com/android/inputmethod/compat/LinearLayoutCompatUtils.java
new file mode 100644
index 000000000..674cbe74b
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/LinearLayoutCompatUtils.java
@@ -0,0 +1,55 @@
+/*
+ * 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.compat;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import java.lang.reflect.Field;
+
+public class LinearLayoutCompatUtils {
+ private static final String TAG = LinearLayoutCompatUtils.class.getSimpleName();
+
+ private static final Class<?> CLASS_R_STYLEABLE = CompatUtils.getClass(
+ "com.android.internal.R$styleable");
+ private static final Field STYLEABLE_VIEW = CompatUtils.getField(
+ CLASS_R_STYLEABLE, "View");
+ private static final Field STYLEABLE_VIEW_BACKGROUND = CompatUtils.getField(
+ CLASS_R_STYLEABLE, "View_background");
+ private static final Object VALUE_STYLEABLE_VIEW = CompatUtils.getFieldValue(
+ null, null, STYLEABLE_VIEW);
+ private static final Integer VALUE_STYLEABLE_VIEW_BACKGROUND =
+ (Integer)CompatUtils.getFieldValue(null, null, STYLEABLE_VIEW_BACKGROUND);
+
+ public static Drawable getBackgroundDrawable(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ if (!(VALUE_STYLEABLE_VIEW instanceof int[]) || VALUE_STYLEABLE_VIEW_BACKGROUND == null) {
+ Log.w(TAG, "Can't get View background attribute using reflection");
+ return null;
+ }
+
+ final int[] styleableView = (int[])VALUE_STYLEABLE_VIEW;
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, styleableView, defStyleAttr, defStyleRes);
+ final Drawable background = a.getDrawable(VALUE_STYLEABLE_VIEW_BACKGROUND);
+ a.recycle();
+ return background;
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java b/java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java
new file mode 100644
index 000000000..8518a4a78
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java
@@ -0,0 +1,23 @@
+/*
+ * 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.compat;
+
+public class MotionEventCompatUtils {
+ public static final int ACTION_HOVER_MOVE = 0x7;
+ public static final int ACTION_HOVER_ENTER = 0x9;
+ public static final int ACTION_HOVER_EXIT = 0xA;
+}
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
new file mode 100644
index 000000000..4929dd948
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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.compat;
+
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver;
+
+import android.content.Context;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class SuggestionSpanUtils {
+ // TODO: Use reflection to get field values
+ public static final String ACTION_SUGGESTION_PICKED =
+ "android.text.style.SUGGESTION_PICKED";
+ public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
+ public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
+ public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
+ public static final int SUGGESTION_MAX_SIZE = 5;
+ public static final boolean SUGGESTION_SPAN_IS_SUPPORTED;
+
+ private static final Class<?> CLASS_SuggestionSpan = CompatUtils
+ .getClass("android.text.style.SuggestionSpan");
+ private static final Class<?>[] INPUT_TYPE_SuggestionSpan = new Class<?>[] {
+ Context.class, Locale.class, String[].class, int.class, Class.class };
+ private static final Constructor<?> CONSTRUCTOR_SuggestionSpan = CompatUtils
+ .getConstructor(CLASS_SuggestionSpan, INPUT_TYPE_SuggestionSpan);
+ static {
+ SUGGESTION_SPAN_IS_SUPPORTED =
+ CLASS_SuggestionSpan != null && CONSTRUCTOR_SuggestionSpan != null;
+ }
+
+ public static CharSequence getTextWithSuggestionSpan(Context context,
+ CharSequence pickedWord, SuggestedWords suggestedWords) {
+ if (TextUtils.isEmpty(pickedWord) || CONSTRUCTOR_SuggestionSpan == null
+ || suggestedWords == null || suggestedWords.size() == 0) {
+ return pickedWord;
+ }
+
+ final Spannable spannable;
+ if (pickedWord instanceof Spannable) {
+ spannable = (Spannable) pickedWord;
+ } else {
+ spannable = new SpannableString(pickedWord);
+ }
+ final ArrayList<String> suggestionsList = new ArrayList<String>();
+ for (int i = 0; i < suggestedWords.size(); ++i) {
+ if (suggestionsList.size() >= SUGGESTION_MAX_SIZE) {
+ break;
+ }
+ final CharSequence word = suggestedWords.getWord(i);
+ if (!TextUtils.equals(pickedWord, word)) {
+ suggestionsList.add(word.toString());
+ }
+ }
+
+ final Object[] args =
+ { context, null, suggestionsList.toArray(new String[suggestionsList.size()]), 0,
+ (Class<?>) SuggestionSpanPickedNotificationReceiver.class };
+ final Object ss = CompatUtils.newInstance(CONSTRUCTOR_SuggestionSpan, args);
+ if (ss == null) {
+ return pickedWord;
+ }
+ spannable.setSpan(ss, 0, pickedWord.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return spannable;
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/VibratorCompatWrapper.java b/java/src/com/android/inputmethod/compat/VibratorCompatWrapper.java
new file mode 100644
index 000000000..8e2a2e0b8
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/VibratorCompatWrapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.compat;
+
+import android.content.Context;
+import android.os.Vibrator;
+
+import java.lang.reflect.Method;
+
+public class VibratorCompatWrapper {
+ private static final Method METHOD_hasVibrator = CompatUtils.getMethod(Vibrator.class,
+ "hasVibrator", int.class);
+
+ private static final VibratorCompatWrapper sInstance = new VibratorCompatWrapper();
+ private Vibrator mVibrator;
+
+ private VibratorCompatWrapper() {
+ }
+
+ public static VibratorCompatWrapper getInstance(Context context) {
+ if (sInstance.mVibrator == null) {
+ sInstance.mVibrator =
+ (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ }
+ return sInstance;
+ }
+
+ public boolean hasVibrator() {
+ if (mVibrator == null)
+ return false;
+ return (Boolean) CompatUtils.invoke(mVibrator, true, METHOD_hasVibrator);
+ }
+}
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/latin/LanguageSwitcher.java b/java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java
index 226b4c690..1eedb5ee1 100644
--- a/java/src/com/android/inputmethod/latin/LanguageSwitcher.java
+++ b/java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 Google Inc.
+ * 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
@@ -14,13 +14,21 @@
* the License.
*/
-package com.android.inputmethod.latin;
+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.preference.PreferenceManager;
+import android.content.res.Configuration;
import android.text.TextUtils;
+import android.util.Log;
+import java.util.ArrayList;
import java.util.Locale;
/**
@@ -28,10 +36,15 @@ import java.util.Locale;
* 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 Locale[] mLocales;
- private LatinIME mIme;
- private String[] mSelectedLanguageArray;
+ 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;
@@ -40,31 +53,43 @@ public class LanguageSwitcher {
public LanguageSwitcher(LatinIME ime) {
mIme = ime;
- mLocales = new Locale[0];
}
- public Locale[] getLocales() {
- return mLocales;
+ public int getLocaleCount() {
+ return mLocales.size();
}
- public int getLocaleCount() {
- return mLocales.length;
+ 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
+ * @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) {
- String selectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, null);
- String currentLanguage = sp.getString(LatinIME.PREF_INPUT_LANGUAGE, null);
- if (selectedLanguages == null || selectedLanguages.length() < 1) {
+ 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.length == 0) {
+ if (mLocales.size() == 0) {
return false;
}
- mLocales = new Locale[0];
+ mLocales.clear();
return true;
}
if (selectedLanguages.equals(mSelectedLanguages)) {
@@ -77,7 +102,7 @@ public class LanguageSwitcher {
if (currentLanguage != null) {
// Find the index
mCurrentIndex = 0;
- for (int i = 0; i < mLocales.length; i++) {
+ for (int i = 0; i < mLocales.size(); i++) {
if (mSelectedLanguageArray[i].equals(currentLanguage)) {
mCurrentIndex = i;
break;
@@ -89,6 +114,9 @@ public class LanguageSwitcher {
}
private void loadDefaults() {
+ if (LatinImeLogger.sDBG) {
+ Log.d(TAG, "load default locales:");
+ }
mDefaultInputLocale = mIme.getResources().getConfiguration().locale;
String country = mDefaultInputLocale.getCountry();
mDefaultInputLanguage = mDefaultInputLocale.getLanguage() +
@@ -96,11 +124,10 @@ public class LanguageSwitcher {
}
private void constructLocales() {
- mLocales = new Locale[mSelectedLanguageArray.length];
- for (int i = 0; i < mLocales.length; i++) {
- final String lang = mSelectedLanguageArray[i];
- mLocales[i] = new Locale(lang.substring(0, 2),
- lang.length() > 4 ? lang.substring(3, 5) : "");
+ mLocales.clear();
+ for (final String lang : mSelectedLanguageArray) {
+ final Locale locale = Utils.constructLocaleFromString(lang);
+ mLocales.add(locale);
}
}
@@ -117,37 +144,47 @@ public class LanguageSwitcher {
/**
* Returns the list of enabled language codes.
*/
- public String[] getEnabledLanguages() {
+ 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.
- * @return
*/
public Locale getInputLocale() {
if (getLocaleCount() == 0) return mDefaultInputLocale;
- return mLocales[mCurrentIndex];
+ 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.
- * @return
*/
public Locale getNextInputLocale() {
if (getLocaleCount() == 0) return mDefaultInputLocale;
-
- return mLocales[(mCurrentIndex + 1) % mLocales.length];
+ return mLocales.get(nextLocaleIndex());
}
/**
* Sets the system locale (display UI) used for comparing with the input language.
* @param locale the locale of the system
*/
- public void setSystemLocale(Locale locale) {
+ private void setSystemLocale(Locale locale) {
mSystemLocale = locale;
}
@@ -155,19 +192,17 @@ public class LanguageSwitcher {
* Returns the system locale.
* @return the system locale
*/
- public Locale getSystemLocale() {
+ 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.
- * @return
*/
public Locale getPrevInputLocale() {
if (getLocaleCount() == 0) return mDefaultInputLocale;
-
- return mLocales[(mCurrentIndex - 1 + mLocales.length) % mLocales.length];
+ return mLocales.get(prevLocaleIndex());
}
public void reset() {
@@ -175,27 +210,25 @@ public class LanguageSwitcher {
}
public void next() {
- mCurrentIndex++;
- if (mCurrentIndex >= mLocales.length) mCurrentIndex = 0; // Wrap around
+ mCurrentIndex = nextLocaleIndex();
}
public void prev() {
- mCurrentIndex--;
- if (mCurrentIndex < 0) mCurrentIndex = mLocales.length - 1; // Wrap around
- }
-
- public void persist() {
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mIme);
- Editor editor = sp.edit();
- editor.putString(LatinIME.PREF_INPUT_LANGUAGE, getInputLanguage());
- SharedPreferencesCompat.apply(editor);
+ mCurrentIndex = prevLocaleIndex();
}
- static String toTitleCase(String s, Locale locale) {
- if (s.length() == 0) {
- return s;
+ 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;
+ }
}
+ }
- return s.toUpperCase(locale).charAt(0) + s.substring(1);
+ 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/voice/FieldContext.java b/java/src/com/android/inputmethod/deprecated/voice/FieldContext.java
index 5fbacfb6c..3c79cc218 100644
--- a/java/src/com/android/inputmethod/voice/FieldContext.java
+++ b/java/src/com/android/inputmethod/deprecated/voice/FieldContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 Google Inc.
+ * 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
@@ -14,7 +14,7 @@
* the License.
*/
-package com.android.inputmethod.voice;
+package com.android.inputmethod.deprecated.voice;
import android.os.Bundle;
import android.util.Log;
@@ -73,6 +73,7 @@ public class FieldContext {
bundle.putInt(IME_OPTIONS, info.imeOptions);
}
+ @SuppressWarnings("static-access")
private static void addInputConnectionToBundle(
InputConnection conn, Bundle bundle) {
if (conn == null) {
@@ -96,6 +97,7 @@ public class FieldContext {
return mFieldInfo;
}
+ @Override
public String toString() {
return mFieldInfo.toString();
}
diff --git a/java/src/com/android/inputmethod/latin/Hints.java b/java/src/com/android/inputmethod/deprecated/voice/Hints.java
index c467365e7..06b234381 100644
--- a/java/src/com/android/inputmethod/latin/Hints.java
+++ b/java/src/com/android/inputmethod/deprecated/voice/Hints.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 Google Inc.
+ * 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
@@ -14,14 +14,14 @@
* the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.deprecated.voice;
-import com.android.inputmethod.voice.SettingsUtil;
+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.preference.PreferenceManager;
import android.view.inputmethod.InputConnection;
import java.util.Calendar;
@@ -47,8 +47,9 @@ public class Hints {
private static final int DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW = 7;
private static final int DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS = 7;
- private Context mContext;
- private Display mDisplay;
+ private final Context mContext;
+ private final SharedPreferences mPrefs;
+ private final Display mDisplay;
private boolean mVoiceResultContainedPunctuation;
private int mSwipeHintMaxDaysToShow;
private int mPunctuationHintMaxDisplays;
@@ -62,8 +63,9 @@ public class Hints {
SPEAKABLE_PUNCTUATION.put("?", "question mark");
}
- public Hints(Context context, Display display) {
+ public Hints(Context context, SharedPreferences prefs, Display display) {
mContext = context;
+ mPrefs = prefs;
mDisplay = display;
ContentResolver cr = mContext.getContentResolver();
@@ -103,8 +105,7 @@ public class Hints {
public void registerVoiceResult(String text) {
// Update the current time as the last time voice input was used.
- SharedPreferences.Editor editor =
- PreferenceManager.getDefaultSharedPreferences(mContext).edit();
+ SharedPreferences.Editor editor = mPrefs.edit();
editor.putLong(PREF_VOICE_INPUT_LAST_TIME_USED, System.currentTimeMillis());
SharedPreferencesCompat.apply(editor);
@@ -118,14 +119,14 @@ public class Hints {
}
private boolean shouldShowSwipeHint() {
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+ final SharedPreferences prefs = mPrefs;
- int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0);
+ 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 = sp.getLong(PREF_VOICE_INPUT_LAST_TIME_USED, 0);
+ 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.)
@@ -156,16 +157,16 @@ public class Hints {
}
private void showHint(int hintViewResource) {
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+ final SharedPreferences prefs = mPrefs;
- int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0);
- long lastTimeHintWasShown = sp.getLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, 0);
+ 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 = sp.edit();
+ 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);
@@ -177,9 +178,9 @@ public class Hints {
}
private int getAndIncrementPref(String pref) {
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
- int value = sp.getInt(pref, 0);
- SharedPreferences.Editor editor = sp.edit();
+ 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/voice/RecognitionView.java b/java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java
index 7cec0b04a..dcb826e8f 100644
--- a/java/src/com/android/inputmethod/voice/RecognitionView.java
+++ b/java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 Google Inc.
+ * 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
@@ -14,16 +14,11 @@
* the License.
*/
-package com.android.inputmethod.voice;
+package com.android.inputmethod.deprecated.voice;
-import java.io.ByteArrayOutputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.ShortBuffer;
-import java.util.ArrayList;
-import java.util.List;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -34,16 +29,20 @@ import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
-import android.util.TypedValue;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
-import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
-import com.android.inputmethod.latin.R;
+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.
@@ -57,80 +56,57 @@ public class RecognitionView {
private View mView;
private Context mContext;
- private ImageView mImage;
private TextView mText;
- private View mButton;
- private TextView mButtonText;
+ private ImageView mImage;
private View mProgress;
+ private SoundIndicator mSoundIndicator;
+ private TextView mLanguage;
+ private Button mButton;
private Drawable mInitializing;
private Drawable mError;
- private List<Drawable> mSpeakNow;
-
- private float mVolume = 0.0f;
- private int mLevel = 0;
-
- private enum State {LISTENING, WORKING, READY}
- private State mState = State.READY;
-
- private float mMinMicrophoneLevel;
- private float mMaxMicrophoneLevel;
- /** Updates the microphone icon to show user their volume.*/
- private Runnable mUpdateVolumeRunnable = new Runnable() {
- public void run() {
- if (mState != State.LISTENING) {
- return;
- }
-
- final float min = mMinMicrophoneLevel;
- final float max = mMaxMicrophoneLevel;
- final int maxLevel = mSpeakNow.size() - 1;
+ 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;
- int index = (int) ((mVolume - min) / (max - min) * maxLevel);
- final int level = Math.min(Math.max(0, index), maxLevel);
+ private final View mPopupLayout;
- if (level != mLevel) {
- mImage.setImageDrawable(mSpeakNow.get(level));
- mLevel = level;
- }
- mUiHandler.postDelayed(mUpdateVolumeRunnable, 50);
- }
- };
+ 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);
+ Context.LAYOUT_INFLATER_SERVICE);
+
mView = inflater.inflate(R.layout.recognition_status, null);
- ContentResolver cr = context.getContentResolver();
- mMinMicrophoneLevel = SettingsUtil.getSettingsFloat(
- cr, SettingsUtil.LATIN_IME_MIN_MICROPHONE_LEVEL, 15.f);
- mMaxMicrophoneLevel = SettingsUtil.getSettingsFloat(
- cr, SettingsUtil.LATIN_IME_MAX_MICROPHONE_LEVEL, 30.f);
+
+ mPopupLayout= mView.findViewById(R.id.popup_layout);
// Pre-load volume level images
Resources r = context.getResources();
- mSpeakNow = new ArrayList<Drawable>();
- mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level0));
- mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level1));
- mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level2));
- mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level3));
- mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level4));
- mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level5));
- mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level6));
+ 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);
- mButton = mView.findViewById(R.id.button);
+ 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);
- mButtonText = (TextView) mView.findViewById(R.id.button_text);
- mProgress = mView.findViewById(R.id.progress);
+ mLanguage = (TextView) mView.findViewById(R.id.language);
mContext = context;
}
@@ -141,11 +117,12 @@ public class RecognitionView {
public void restoreState() {
mUiHandler.post(new Runnable() {
+ @Override
public void run() {
// Restart the spinner
- if (mState == State.WORKING) {
- ((ProgressBar)mProgress).setIndeterminate(false);
- ((ProgressBar)mProgress).setIndeterminate(true);
+ if (mState == WORKING) {
+ ((ProgressBar) mProgress).setIndeterminate(false);
+ ((ProgressBar) mProgress).setIndeterminate(true);
}
}
});
@@ -153,46 +130,50 @@ public class RecognitionView {
public void showInitializing() {
mUiHandler.post(new Runnable() {
+ @Override
public void run() {
- prepareDialog(false, mContext.getText(R.string.voice_initializing), mInitializing,
- mContext.getText(R.string.cancel));
+ 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 = State.LISTENING;
- prepareDialog(false, mContext.getText(R.string.voice_listening), mSpeakNow.get(0),
+ mState = LISTENING;
+ prepareDialog(mContext.getText(R.string.voice_listening), null,
mContext.getText(R.string.cancel));
}
});
- mUiHandler.postDelayed(mUpdateVolumeRunnable, 50);
}
- public void updateVoiceMeter(final float rmsdB) {
- mVolume = rmsdB;
+ public void updateVoiceMeter(float rmsdB) {
+ mSoundIndicator.setRmsdB(rmsdB);
}
public void showError(final String message) {
mUiHandler.post(new Runnable() {
+ @Override
public void run() {
- mState = State.READY;
- prepareDialog(false, message, mError, mContext.getText(R.string.ok));
+ 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 = State.WORKING;
- prepareDialog(true, mContext.getText(R.string.voice_working), null, mContext
+ 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();
@@ -200,21 +181,87 @@ public class RecognitionView {
waveBuffer.reset();
showWave(buf, speechStartPosition / 2, speechEndPosition / 2);
}
- });
+ });
}
- private void prepareDialog(boolean spinVisible, CharSequence text, Drawable image,
+ private void prepareDialog(CharSequence text, Drawable image,
CharSequence btnTxt) {
- if (spinVisible) {
- mProgress.setVisibility(View.VISIBLE);
- mImage.setVisibility(View.GONE);
- } else {
- mProgress.setVisibility(View.GONE);
- mImage.setImageDrawable(image);
- mImage.setVisibility(View.VISIBLE);
+
+ /*
+ * 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);
}
- mText.setText(text);
- mButtonText.setText(btnTxt);
+ mPopupLayout.requestLayout();
+ mButton.setText(btnTxt);
}
/**
@@ -241,7 +288,7 @@ public class RecognitionView {
*/
private void showWave(ShortBuffer waveBuffer, int startPosition, int endPosition) {
final int w = ((View) mImage.getParent()).getWidth();
- final int h = mImage.getHeight();
+ final int h = ((View) mImage.getParent()).getHeight();
if (w <= 0 || h <= 0) {
// view is not visible this time. Skip drawing.
return;
@@ -252,7 +299,7 @@ public class RecognitionView {
paint.setColor(0xFFFFFFFF); // 0xAARRGGBB
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
- paint.setAlpha(0x90);
+ paint.setAlpha(80);
final PathEffect effect = new CornerPathEffect(3);
paint.setPathEffect(effect);
@@ -274,7 +321,7 @@ public class RecognitionView {
final int count = (endIndex - startIndex) / numSamplePerWave;
final float deltaX = 1.0f * w / count;
- int yMax = h / 2 - 8;
+ int yMax = h / 2;
Path path = new Path();
c.translate(0, yMax);
float x = 0;
@@ -288,36 +335,20 @@ public class RecognitionView {
path.lineTo(x, y);
}
if (deltaX > 4) {
- paint.setStrokeWidth(3);
+ paint.setStrokeWidth(2);
} else {
- paint.setStrokeWidth(Math.max(1, (int) (deltaX -.05)));
+ paint.setStrokeWidth(Math.max(0, (int) (deltaX -.05)));
}
c.drawPath(path, paint);
mImage.setImageBitmap(b);
- mImage.setVisibility(View.VISIBLE);
- MarginLayoutParams mProgressParams = (MarginLayoutParams)mProgress.getLayoutParams();
- mProgressParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
- -h , mContext.getResources().getDisplayMetrics());
-
- // Tweak the padding manually to fill out the whole view horizontally.
- // TODO: Do this in the xml layout instead.
- ((View) mImage.getParent()).setPadding(4, ((View) mImage.getParent()).getPaddingTop(), 3,
- ((View) mImage.getParent()).getPaddingBottom());
- mProgress.setLayoutParams(mProgressParams);
}
-
public void finish() {
mUiHandler.post(new Runnable() {
+ @Override
public void run() {
- mState = State.READY;
- exitWorking();
+ mSoundIndicator.stop();
}
- });
- }
-
- private void exitWorking() {
- mProgress.setVisibility(View.GONE);
- mImage.setVisibility(View.VISIBLE);
+ });
}
}
diff --git a/java/src/com/android/inputmethod/voice/SettingsUtil.java b/java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java
index abf52047f..855a09a1d 100644
--- a/java/src/com/android/inputmethod/voice/SettingsUtil.java
+++ b/java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 Google Inc.
+ * 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
@@ -14,13 +14,10 @@
* the License.
*/
-package com.android.inputmethod.voice;
+package com.android.inputmethod.deprecated.voice;
import android.content.ContentResolver;
-import android.database.Cursor;
-import android.net.Uri;
import android.provider.Settings;
-import android.util.Log;
/**
* Utility for retrieving settings from Settings.Secure.
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/voice/VoiceInput.java b/java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java
index 4c54dd3c5..b718ebbb7 100644
--- a/java/src/com/android/inputmethod/voice/VoiceInput.java
+++ b/java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 Google Inc.
+ * 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
@@ -14,26 +14,28 @@
* the License.
*/
-package com.android.inputmethod.voice;
+package com.android.inputmethod.deprecated.voice;
-import com.android.inputmethod.latin.EditingUtil;
+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.SpeechRecognizer;
import android.speech.RecognizerIntent;
+import android.speech.SpeechRecognizer;
import android.util.Log;
-import android.view.inputmethod.InputConnection;
import android.view.View;
import android.view.View.OnClickListener;
+import android.view.inputmethod.InputConnection;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -57,6 +59,7 @@ public class VoiceInput implements OnClickListener {
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 " +
@@ -86,6 +89,7 @@ public class VoiceInput implements OnClickListener {
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";
@@ -125,13 +129,13 @@ public class VoiceInput implements OnClickListener {
private int mAfterVoiceInputSelectionSpan = 0;
private int mState = DEFAULT;
-
- private final static int MSG_CLOSE_ERROR_DIALOG = 1;
+
+ private final static int MSG_RESET = 1;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
- if (msg.what == MSG_CLOSE_ERROR_DIALOG) {
+ if (msg.what == MSG_RESET) {
mState = DEFAULT;
mRecognitionView.finish();
mUiListener.onCancelVoice();
@@ -188,7 +192,7 @@ public class VoiceInput implements OnClickListener {
}
mBlacklist = new Whitelist();
- mBlacklist.addApp("com.android.setupwizard");
+ mBlacklist.addApp("com.google.android.setupwizard");
}
public void setCursorPos(int pos) {
@@ -240,7 +244,7 @@ public class VoiceInput implements OnClickListener {
}
public void incrementTextModificationInsertPunctuationCount(int count){
- mAfterVoiceInputInsertPunctuationCount += 1;
+ mAfterVoiceInputInsertPunctuationCount += count;
if (mAfterVoiceInputSelectionSpan > 0) {
// If text was highlighted before inserting the char, count this as
// a delete.
@@ -276,8 +280,9 @@ public class VoiceInput implements OnClickListener {
* 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() {
+ public void onConfigurationChanged(Configuration configuration) {
mRecognitionView.restoreState();
+ mRecognitionView.getView().dispatchConfigurationChanged(configuration);
}
/**
@@ -305,8 +310,18 @@ public class VoiceInput implements OnClickListener {
* @param swipe whether this voice input was started by swipe, for logging purposes
*/
public void startListening(FieldContext context, boolean swipe) {
- mState = DEFAULT;
-
+ 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();
@@ -405,6 +420,7 @@ public class VoiceInput implements OnClickListener {
/**
* Handle the cancel button.
*/
+ @Override
public void onClick(View view) {
switch(view.getId()) {
case R.id.button:
@@ -427,8 +443,7 @@ public class VoiceInput implements OnClickListener {
public void logTextModifiedByChooseSuggestion(String suggestion, int index,
String wordSeparators, InputConnection ic) {
- EditingUtil.Range range = new EditingUtil.Range();
- String wordToBeReplaced = EditingUtil.getWordAtCursor(ic, wordSeparators, range);
+ 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(),
@@ -491,6 +506,21 @@ public class VoiceInput implements OnClickListener {
}
/**
+ * 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() {
@@ -505,14 +535,9 @@ public class VoiceInput implements OnClickListener {
mLogger.cancelDuringError();
break;
}
- mState = DEFAULT;
- // Remove all pending tasks (e.g., timers to cancel voice input)
- mHandler.removeMessages(MSG_CLOSE_ERROR_DIALOG);
-
- mSpeechRecognizer.cancel();
+ reset();
mUiListener.onCancelVoice();
- mRecognitionView.finish();
}
private int getErrorStringId(int errorType, boolean endpointed) {
@@ -547,7 +572,7 @@ public class VoiceInput implements OnClickListener {
mState = ERROR;
mRecognitionView.showError(error);
// Wait a couple seconds and then automatically dismiss message.
- mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_CLOSE_ERROR_DIALOG), 2000);
+ mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_RESET), 2000);
}
private class ImeRecognitionListener implements RecognitionListener {
@@ -556,36 +581,45 @@ public class VoiceInput implements OnClickListener {
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) {}
+ } 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);
@@ -638,10 +672,12 @@ public class VoiceInput implements OnClickListener {
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/voice/VoiceInputLogger.java b/java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java
index 0d21133e0..22e8207bf 100644
--- a/java/src/com/android/inputmethod/voice/VoiceInputLogger.java
+++ b/java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008 Google Inc.
+ * 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
@@ -14,10 +14,10 @@
* the License.
*/
-package com.android.inputmethod.voice;
+package com.android.inputmethod.deprecated.voice;
import com.android.common.speech.LoggingEvents;
-import com.android.common.userhappiness.UserHappinessSignals;
+import com.android.inputmethod.deprecated.compat.VoiceInputLoggerCompatUtils;
import android.content.Context;
import android.content.Intent;
@@ -31,6 +31,7 @@ import android.content.Intent;
* on on the VoiceSearch side.
*/
public class VoiceInputLogger {
+ @SuppressWarnings("unused")
private static final String TAG = VoiceInputLogger.class.getSimpleName();
private static VoiceInputLogger sVoiceInputLogger;
@@ -205,17 +206,18 @@ public class VoiceInputLogger {
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_TYPE,
- LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_CHOOSE_SUGGESTION);
i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, suggestionLength);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_REPLACED_LENGTH, replacedPhraseLength);
+ 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(LoggingEvents.VoiceIme.EXTRA_BEFORE_N_BEST_CHOOSE, before);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_AFTER_N_BEST_CHOOSE, after);
+ i.putExtra(VoiceInputLoggerCompatUtils.EXTRA_BEFORE_N_BEST_CHOOSE, before);
+ i.putExtra(VoiceInputLoggerCompatUtils.EXTRA_AFTER_N_BEST_CHOOSE, after);
mContext.sendBroadcast(i);
}
@@ -254,7 +256,7 @@ public class VoiceInputLogger {
// 2. type subject in subject field
// 3. speak message in message field
// 4. press send
- UserHappinessSignals.setHasVoiceLoggingInfo(hasLoggingInfo);
+ VoiceInputLoggerCompatUtils.setHasVoiceLoggingInfoCompat(hasLoggingInfo);
}
private boolean hasLoggingInfo(){
diff --git a/java/src/com/android/inputmethod/voice/WaveformImage.java b/java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java
index 08d87c8f3..8ed279f42 100644
--- a/java/src/com/android/inputmethod/voice/WaveformImage.java
+++ b/java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008-2009 Google Inc.
+ * 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
@@ -14,7 +14,7 @@
* the License.
*/
-package com.android.inputmethod.voice;
+package com.android.inputmethod.deprecated.voice;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -33,7 +33,9 @@ import java.nio.ShortBuffer;
public class WaveformImage {
private static final int SAMPLING_RATE = 8000;
- private WaveformImage() {}
+ private WaveformImage() {
+ // Intentional empty constructor.
+ }
public static Bitmap drawWaveform(
ByteArrayOutputStream waveBuffer, int w, int h, int start, int end) {
diff --git a/java/src/com/android/inputmethod/voice/Whitelist.java b/java/src/com/android/inputmethod/deprecated/voice/Whitelist.java
index 167b688ca..6c5f52ae2 100644
--- a/java/src/com/android/inputmethod/voice/Whitelist.java
+++ b/java/src/com/android/inputmethod/deprecated/voice/Whitelist.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 Google Inc.
+ * 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
@@ -14,9 +14,10 @@
* the License.
*/
-package com.android.inputmethod.voice;
+package com.android.inputmethod.deprecated.voice;
import android.os.Bundle;
+
import java.util.ArrayList;
import java.util.List;
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
new file mode 100644
index 000000000..2850c95df
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -0,0 +1,474 @@
+/*
+ * 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.keyboard;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Xml;
+
+import com.android.inputmethod.keyboard.internal.KeyStyles;
+import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.keyboard.internal.KeyboardParser;
+import com.android.inputmethod.keyboard.internal.PopupCharactersParser;
+import com.android.inputmethod.keyboard.internal.Row;
+import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle;
+import com.android.inputmethod.keyboard.internal.KeyboardParser.ParseException;
+import com.android.inputmethod.latin.R;
+
+import java.util.ArrayList;
+
+/**
+ * Class for describing the position and characteristics of a single key in the keyboard.
+ */
+public class Key {
+ /**
+ * The key code (unicode or custom code) that this key generates.
+ */
+ public final int mCode;
+
+ /** Label to display */
+ public final CharSequence mLabel;
+ /** Hint letter to display on the key in conjunction with the label */
+ public final CharSequence mHintLetter;
+ /** Option of the label */
+ public final int mLabelOption;
+ public static final int LABEL_OPTION_ALIGN_LEFT = 0x01;
+ public static final int LABEL_OPTION_ALIGN_RIGHT = 0x02;
+ public static final int LABEL_OPTION_ALIGN_BOTTOM = 0x08;
+ public static final int LABEL_OPTION_FONT_NORMAL = 0x10;
+ public static final int LABEL_OPTION_FONT_FIXED_WIDTH = 0x20;
+ public static final int LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO = 0x40;
+ private static final int LABEL_OPTION_POPUP_HINT = 0x80;
+ private static final int LABEL_OPTION_HAS_UPPERCASE_LETTER = 0x100;
+
+ /** Icon to display instead of a label. Icon takes precedence over a label */
+ private Drawable mIcon;
+ /** Preview version of the icon, for the preview popup */
+ private Drawable mPreviewIcon;
+
+ /** Width of the key, not including the gap */
+ public final int mWidth;
+ /** Height of the key, not including the gap */
+ public final int mHeight;
+ /** The horizontal gap around this key */
+ public final int mGap;
+ /** The visual insets */
+ public final int mVisualInsetsLeft;
+ public final int mVisualInsetsRight;
+ /** Whether this key is sticky, i.e., a toggle key */
+ public final boolean mSticky;
+ /** X coordinate of the key in the keyboard layout */
+ public final int mX;
+ /** Y coordinate of the key in the keyboard layout */
+ public final int mY;
+ /** Text to output when pressed. This can be multiple characters, like ".com" */
+ public final CharSequence mOutputText;
+ /** Popup characters */
+ public final CharSequence[] mPopupCharacters;
+ /** Popup keyboard maximum column number */
+ public final int mMaxPopupColumn;
+
+ /**
+ * Flags that specify the anchoring to edges of the keyboard for detecting touch events
+ * that are just out of the boundary of the key. This is a bit mask of
+ * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT},
+ * {@link Keyboard#EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM}.
+ */
+ public final int mEdgeFlags;
+ /** Whether this is a functional key which has different key top than normal key */
+ public final boolean mFunctional;
+ /** Whether this key repeats itself when held down */
+ public final boolean mRepeatable;
+
+ /** The Keyboard that this key belongs to */
+ private final Keyboard mKeyboard;
+
+ /** The current pressed state of this key */
+ private boolean mPressed;
+ /** If this is a sticky key, is its highlight on? */
+ private boolean mHighlightOn;
+ /** Key is enabled and responds on press */
+ private boolean mEnabled = true;
+
+ // keyWidth constants
+ private static final int KEYWIDTH_FILL_RIGHT = 0;
+ private static final int KEYWIDTH_FILL_BOTH = -1;
+
+ private final static int[] KEY_STATE_NORMAL_ON = {
+ android.R.attr.state_checkable,
+ android.R.attr.state_checked
+ };
+
+ private final static int[] KEY_STATE_PRESSED_ON = {
+ android.R.attr.state_pressed,
+ android.R.attr.state_checkable,
+ android.R.attr.state_checked
+ };
+
+ private final static int[] KEY_STATE_NORMAL_OFF = {
+ android.R.attr.state_checkable
+ };
+
+ private final static int[] KEY_STATE_PRESSED_OFF = {
+ android.R.attr.state_pressed,
+ android.R.attr.state_checkable
+ };
+
+ private final static int[] KEY_STATE_NORMAL = {
+ };
+
+ private final static int[] KEY_STATE_PRESSED = {
+ android.R.attr.state_pressed
+ };
+
+ // functional normal state (with properties)
+ private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
+ android.R.attr.state_single
+ };
+
+ // functional pressed state (with properties)
+ private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
+ android.R.attr.state_single,
+ android.R.attr.state_pressed
+ };
+
+ /**
+ * This constructor is being used only for key in popup mini keyboard.
+ */
+ public Key(Resources res, Keyboard keyboard, CharSequence popupCharacter, int x, int y,
+ int width, int height, int edgeFlags) {
+ mKeyboard = keyboard;
+ mHeight = height - keyboard.getVerticalGap();
+ mGap = keyboard.getHorizontalGap();
+ mVisualInsetsLeft = mVisualInsetsRight = 0;
+ mWidth = width - mGap;
+ mEdgeFlags = edgeFlags;
+ mHintLetter = null;
+ mLabelOption = 0;
+ mFunctional = false;
+ mSticky = false;
+ mRepeatable = false;
+ mPopupCharacters = null;
+ mMaxPopupColumn = 0;
+ final String popupSpecification = popupCharacter.toString();
+ mLabel = PopupCharactersParser.getLabel(popupSpecification);
+ mOutputText = PopupCharactersParser.getOutputText(popupSpecification);
+ mCode = PopupCharactersParser.getCode(res, popupSpecification);
+ mIcon = keyboard.mIconsSet.getIcon(PopupCharactersParser.getIconId(popupSpecification));
+ // Horizontal gap is divided equally to both sides of the key.
+ mX = x + mGap / 2;
+ mY = y;
+ }
+
+ /**
+ * Create a key with the given top-left coordinate and extract its attributes from the XML
+ * parser.
+ * @param res resources associated with the caller's context
+ * @param row the row that this key belongs to. The row must already be attached to
+ * a {@link Keyboard}.
+ * @param x the x coordinate of the top-left
+ * @param y the y coordinate of the top-left
+ * @param parser the XML parser containing the attributes for this key
+ * @param keyStyles active key styles set
+ */
+ public Key(Resources res, Row row, int x, int y, XmlResourceParser parser,
+ KeyStyles keyStyles) {
+ mKeyboard = row.getKeyboard();
+
+ final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard);
+ int keyWidth;
+ try {
+ mHeight = KeyboardParser.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_rowHeight,
+ mKeyboard.getKeyboardHeight(), row.mDefaultHeight) - row.mVerticalGap;
+ mGap = KeyboardParser.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_horizontalGap,
+ mKeyboard.getDisplayWidth(), row.mDefaultHorizontalGap);
+ keyWidth = KeyboardParser.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_keyWidth,
+ mKeyboard.getDisplayWidth(), row.mDefaultWidth);
+ } finally {
+ keyboardAttr.recycle();
+ }
+
+ final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Key);
+ try {
+ final KeyStyle style;
+ if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
+ String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle);
+ style = keyStyles.getKeyStyle(styleName);
+ if (style == null)
+ throw new ParseException("Unknown key style: " + styleName, parser);
+ } else {
+ style = keyStyles.getEmptyKeyStyle();
+ }
+
+ final int keyboardWidth = mKeyboard.getDisplayWidth();
+ int keyXPos = KeyboardParser.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyXPos, keyboardWidth, x);
+ if (keyXPos < 0) {
+ // If keyXPos is negative, the actual x-coordinate will be k + keyXPos.
+ keyXPos += keyboardWidth;
+ if (keyXPos < x) {
+ // keyXPos shouldn't be less than x because drawable area for this key starts
+ // at x. Or, this key will overlaps the adjacent key on its left hand side.
+ keyXPos = x;
+ }
+ }
+ if (keyWidth == KEYWIDTH_FILL_RIGHT) {
+ // If keyWidth is zero, the actual key width will be determined to fill out the
+ // area up to the right edge of the keyboard.
+ keyWidth = keyboardWidth - keyXPos;
+ } else if (keyWidth <= KEYWIDTH_FILL_BOTH) {
+ // If keyWidth is negative, the actual key width will be determined to fill out the
+ // area between the nearest key on the left hand side and the right edge of the
+ // keyboard.
+ keyXPos = x;
+ keyWidth = keyboardWidth - keyXPos;
+ }
+
+ // Horizontal gap is divided equally to both sides of the key.
+ mX = keyXPos + mGap / 2;
+ mY = y;
+ mWidth = keyWidth - mGap;
+
+ final CharSequence[] popupCharacters = style.getTextArray(keyAttr,
+ R.styleable.Keyboard_Key_popupCharacters);
+ if (res.getBoolean(R.bool.config_digit_popup_characters_enabled)) {
+ mPopupCharacters = popupCharacters;
+ } else {
+ mPopupCharacters = filterOutDigitPopupCharacters(popupCharacters);
+ }
+ mMaxPopupColumn = style.getInt(keyboardAttr,
+ R.styleable.Keyboard_Key_maxPopupKeyboardColumn,
+ mKeyboard.getMaxPopupKeyboardColumn());
+
+ mRepeatable = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable, false);
+ mFunctional = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isFunctional, false);
+ mSticky = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isSticky, false);
+ mEnabled = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_enabled, true);
+ mEdgeFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyEdgeFlags, 0)
+ | row.mRowEdgeFlags;
+
+ final KeyboardIconsSet iconsSet = mKeyboard.mIconsSet;
+ mVisualInsetsLeft = KeyboardParser.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_visualInsetsLeft, mKeyboard.getDisplayHeight(), 0);
+ mVisualInsetsRight = KeyboardParser.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_visualInsetsRight, mKeyboard.getDisplayHeight(), 0);
+ mPreviewIcon = iconsSet.getIcon(style.getInt(
+ keyAttr, R.styleable.Keyboard_Key_keyIconPreview,
+ KeyboardIconsSet.ICON_UNDEFINED));
+ Keyboard.setDefaultBounds(mPreviewIcon);
+ mIcon = iconsSet.getIcon(style.getInt(
+ keyAttr, R.styleable.Keyboard_Key_keyIcon,
+ KeyboardIconsSet.ICON_UNDEFINED));
+ Keyboard.setDefaultBounds(mIcon);
+ mHintLetter = style.getText(keyAttr, R.styleable.Keyboard_Key_keyHintLetter);
+
+ mLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyLabel);
+ mLabelOption = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption, 0);
+ mOutputText = style.getText(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
+ // Choose the first letter of the label as primary code if not
+ // specified.
+ final int code = style.getInt(keyAttr, R.styleable.Keyboard_Key_code,
+ Keyboard.CODE_UNSPECIFIED);
+ if (code == Keyboard.CODE_UNSPECIFIED && !TextUtils.isEmpty(mLabel)) {
+ mCode = mLabel.charAt(0);
+ } else if (code != Keyboard.CODE_UNSPECIFIED) {
+ mCode = code;
+ } else {
+ mCode = Keyboard.CODE_DUMMY;
+ }
+
+ final Drawable shiftedIcon = iconsSet.getIcon(style.getInt(
+ keyAttr, R.styleable.Keyboard_Key_keyIconShifted,
+ KeyboardIconsSet.ICON_UNDEFINED));
+ if (shiftedIcon != null)
+ mKeyboard.getShiftedIcons().put(this, shiftedIcon);
+ } finally {
+ keyAttr.recycle();
+ }
+ }
+
+ public boolean hasPopupHint() {
+ return (mLabelOption & LABEL_OPTION_POPUP_HINT) != 0;
+ }
+
+ public boolean hasUppercaseLetter() {
+ return (mLabelOption & LABEL_OPTION_HAS_UPPERCASE_LETTER) != 0;
+ }
+
+ private static boolean isDigitPopupCharacter(CharSequence label) {
+ return label != null && label.length() == 1 && Character.isDigit(label.charAt(0));
+ }
+
+ private static CharSequence[] filterOutDigitPopupCharacters(CharSequence[] popupCharacters) {
+ if (popupCharacters == null || popupCharacters.length < 1)
+ return null;
+ if (popupCharacters.length == 1 && isDigitPopupCharacter(
+ PopupCharactersParser.getLabel(popupCharacters[0].toString())))
+ return null;
+ ArrayList<CharSequence> filtered = null;
+ for (int i = 0; i < popupCharacters.length; i++) {
+ final CharSequence popupSpec = popupCharacters[i];
+ if (isDigitPopupCharacter(PopupCharactersParser.getLabel(popupSpec.toString()))) {
+ if (filtered == null) {
+ filtered = new ArrayList<CharSequence>();
+ for (int j = 0; j < i; j++)
+ filtered.add(popupCharacters[j]);
+ }
+ } else if (filtered != null) {
+ filtered.add(popupSpec);
+ }
+ }
+ if (filtered == null)
+ return popupCharacters;
+ if (filtered.size() == 0)
+ return null;
+ return filtered.toArray(new CharSequence[filtered.size()]);
+ }
+
+ public Drawable getIcon() {
+ return mIcon;
+ }
+
+ public Drawable getPreviewIcon() {
+ return mPreviewIcon;
+ }
+
+ public void setIcon(Drawable icon) {
+ mIcon = icon;
+ }
+
+ public void setPreviewIcon(Drawable icon) {
+ mPreviewIcon = icon;
+ }
+
+ /**
+ * Informs the key that it has been pressed, in case it needs to change its appearance or
+ * state.
+ * @see #onReleased()
+ */
+ public void onPressed() {
+ mPressed = true;
+ }
+
+ /**
+ * Informs the key that it has been released, in case it needs to change its appearance or
+ * state.
+ * @see #onPressed()
+ */
+ public void onReleased() {
+ mPressed = false;
+ }
+
+ public void setHighlightOn(boolean highlightOn) {
+ mHighlightOn = highlightOn;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ /**
+ * Detects if a point falls on this key.
+ * @param x the x-coordinate of the point
+ * @param y the y-coordinate of the point
+ * @return whether or not the point falls on the key. If the key is attached to an edge, it will
+ * assume that all points between the key and the edge are considered to be on the key.
+ */
+ public boolean isOnKey(int x, int y) {
+ final int flags = mEdgeFlags;
+ final boolean leftEdge = (flags & Keyboard.EDGE_LEFT) != 0;
+ final boolean rightEdge = (flags & Keyboard.EDGE_RIGHT) != 0;
+ final boolean topEdge = (flags & Keyboard.EDGE_TOP) != 0;
+ final boolean bottomEdge = (flags & Keyboard.EDGE_BOTTOM) != 0;
+ final int left = mX - mGap / 2;
+ final int right = left + mWidth + mGap;
+ final int top = mY;
+ final int bottom = top + mHeight + mKeyboard.getVerticalGap();
+ // In order to mitigate rounding errors, we use (left <= x <= right) here.
+ return (x >= left || leftEdge) && (x <= right || rightEdge)
+ && (y >= top || topEdge) && (y <= bottom || bottomEdge);
+ }
+
+ /**
+ * Returns the square of the distance to the nearest edge of the key and the given point.
+ * @param x the x-coordinate of the point
+ * @param y the y-coordinate of the point
+ * @return the square of the distance of the point from the nearest edge of the key
+ */
+ public int squaredDistanceToEdge(int x, int y) {
+ final int left = mX;
+ final int right = left + mWidth;
+ final int top = mY;
+ final int bottom = top + mHeight;
+ final int edgeX = x < left ? left : (x > right ? right : x);
+ final int edgeY = y < top ? top : (y > bottom ? bottom : y);
+ final int dx = x - edgeX;
+ final int dy = y - edgeY;
+ return dx * dx + dy * dy;
+ }
+
+ /**
+ * Returns the drawable state for the key, based on the current state and type of the key.
+ * @return the drawable state of the key.
+ * @see android.graphics.drawable.StateListDrawable#setState(int[])
+ */
+ public int[] getCurrentDrawableState() {
+ final boolean pressed = mPressed;
+ if (!mSticky && mFunctional) {
+ if (pressed) {
+ return KEY_STATE_FUNCTIONAL_PRESSED;
+ } else {
+ return KEY_STATE_FUNCTIONAL_NORMAL;
+ }
+ }
+
+ int[] states = KEY_STATE_NORMAL;
+
+ if (mHighlightOn) {
+ if (pressed) {
+ states = KEY_STATE_PRESSED_ON;
+ } else {
+ states = KEY_STATE_NORMAL_ON;
+ }
+ } else {
+ if (mSticky) {
+ if (pressed) {
+ states = KEY_STATE_PRESSED_OFF;
+ } else {
+ states = KEY_STATE_NORMAL_OFF;
+ }
+ } else {
+ if (pressed) {
+ states = KEY_STATE_PRESSED;
+ }
+ }
+ }
+ return states;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
new file mode 100644
index 000000000..7add43a6d
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -0,0 +1,202 @@
+/*
+ * 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.keyboard;
+
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class KeyDetector {
+ private static final String TAG = KeyDetector.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ public static final int NOT_A_CODE = -1;
+ public static final int NOT_A_KEY = -1;
+
+ private Keyboard mKeyboard;
+ private int mCorrectionX;
+ private int mCorrectionY;
+ private boolean mProximityCorrectOn;
+ private int mProximityThresholdSquare;
+
+ // working area
+ private static final int MAX_NEARBY_KEYS = 12;
+ private final int[] mDistances = new int[MAX_NEARBY_KEYS];
+ private final int[] mIndices = new int[MAX_NEARBY_KEYS];
+
+ public void setKeyboard(Keyboard keyboard, float correctionX, float correctionY) {
+ if (keyboard == null)
+ throw new NullPointerException();
+ mCorrectionX = (int)correctionX;
+ mCorrectionY = (int)correctionY;
+ mKeyboard = keyboard;
+ }
+
+ protected int getTouchX(int x) {
+ return x + mCorrectionX;
+ }
+
+ protected int getTouchY(int y) {
+ return y + mCorrectionY;
+ }
+
+ protected List<Key> getKeys() {
+ if (mKeyboard == null)
+ throw new IllegalStateException("keyboard isn't set");
+ // mKeyboard is guaranteed not to be null at setKeybaord() method if mKeys is not null
+ return mKeyboard.getKeys();
+ }
+
+ public void setProximityCorrectionEnabled(boolean enabled) {
+ mProximityCorrectOn = enabled;
+ }
+
+ public boolean isProximityCorrectionEnabled() {
+ return mProximityCorrectOn;
+ }
+
+ public void setProximityThreshold(int threshold) {
+ mProximityThresholdSquare = threshold * threshold;
+ }
+
+ /**
+ * Computes maximum size of the array that can contain all nearby key indices returned by
+ * {@link #getKeyIndexAndNearbyCodes}.
+ *
+ * @return Returns maximum size of the array that can contain all nearby key indices returned
+ * by {@link #getKeyIndexAndNearbyCodes}.
+ */
+ protected int getMaxNearbyKeys() {
+ return MAX_NEARBY_KEYS;
+ }
+
+ /**
+ * Allocates array that can hold all key indices returned by {@link #getKeyIndexAndNearbyCodes}
+ * method. The maximum size of the array should be computed by {@link #getMaxNearbyKeys}.
+ *
+ * @return Allocates and returns an array that can hold all key indices returned by
+ * {@link #getKeyIndexAndNearbyCodes} method. All elements in the returned array are
+ * initialized by {@link #NOT_A_CODE} value.
+ */
+ public int[] newCodeArray() {
+ int[] codes = new int[getMaxNearbyKeys()];
+ Arrays.fill(codes, NOT_A_CODE);
+ return codes;
+ }
+
+ private void initializeNearbyKeys() {
+ Arrays.fill(mDistances, Integer.MAX_VALUE);
+ Arrays.fill(mIndices, NOT_A_KEY);
+ }
+
+ /**
+ * Insert the key into nearby keys buffer and sort nearby keys by ascending order of distance.
+ * If the distance of two keys are the same, the key which the point is on should be considered
+ * as a closer one.
+ *
+ * @param keyIndex index of the key.
+ * @param distance distance between the key's edge and user touched point.
+ * @param isOnKey true if the point is on the key.
+ * @return order of the key in the nearby buffer, 0 if it is the nearest key.
+ */
+ private int sortNearbyKeys(int keyIndex, int distance, boolean isOnKey) {
+ final int[] distances = mDistances;
+ final int[] indices = mIndices;
+ for (int insertPos = 0; insertPos < distances.length; insertPos++) {
+ final int comparingDistance = distances[insertPos];
+ if (distance < comparingDistance || (distance == comparingDistance && isOnKey)) {
+ final int nextPos = insertPos + 1;
+ if (nextPos < distances.length) {
+ System.arraycopy(distances, insertPos, distances, nextPos,
+ distances.length - nextPos);
+ System.arraycopy(indices, insertPos, indices, nextPos,
+ indices.length - nextPos);
+ }
+ distances[insertPos] = distance;
+ indices[insertPos] = keyIndex;
+ return insertPos;
+ }
+ }
+ return distances.length;
+ }
+
+ private void getNearbyKeyCodes(final int[] allCodes) {
+ final List<Key> keys = getKeys();
+ final int[] indices = mIndices;
+
+ // allCodes[0] should always have the key code even if it is a non-letter key.
+ if (indices[0] == NOT_A_KEY) {
+ allCodes[0] = NOT_A_CODE;
+ return;
+ }
+
+ int numCodes = 0;
+ for (int j = 0; j < indices.length && numCodes < allCodes.length; j++) {
+ final int index = indices[j];
+ if (index == NOT_A_KEY)
+ break;
+ final int code = keys.get(index).mCode;
+ // filter out a non-letter key from nearby keys
+ if (code < Keyboard.CODE_SPACE)
+ continue;
+ allCodes[numCodes++] = code;
+ }
+ }
+
+ /**
+ * Finds all possible nearby key indices around a touch event point and returns the nearest key
+ * index. The algorithm to determine the nearby keys depends on the threshold set by
+ * {@link #setProximityThreshold(int)} and the mode set by
+ * {@link #setProximityCorrectionEnabled(boolean)}.
+ *
+ * @param x The x-coordinate of a touch point
+ * @param y The y-coordinate of a touch point
+ * @param allCodes All nearby key code except functional key are returned in this array
+ * @return The nearest key index
+ */
+ public int getKeyIndexAndNearbyCodes(int x, int y, final int[] allCodes) {
+ final List<Key> keys = getKeys();
+ final int touchX = getTouchX(x);
+ final int touchY = getTouchY(y);
+
+ initializeNearbyKeys();
+ int primaryIndex = NOT_A_KEY;
+ for (final int index : mKeyboard.getNearestKeys(touchX, touchY)) {
+ final Key key = keys.get(index);
+ final boolean isOnKey = key.isOnKey(touchX, touchY);
+ final int distance = key.squaredDistanceToEdge(touchX, touchY);
+ if (isOnKey || (mProximityCorrectOn && distance < mProximityThresholdSquare)) {
+ final int insertedPosition = sortNearbyKeys(index, distance, isOnKey);
+ if (insertedPosition == 0 && isOnKey)
+ primaryIndex = index;
+ }
+ }
+
+ if (allCodes != null && allCodes.length > 0) {
+ getNearbyKeyCodes(allCodes);
+ if (DEBUG) {
+ Log.d(TAG, "x=" + x + " y=" + y
+ + " primary="
+ + (primaryIndex == NOT_A_KEY ? "none" : keys.get(primaryIndex).mCode)
+ + " codes=" + Arrays.toString(allCodes));
+ }
+ }
+
+ return primaryIndex;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
new file mode 100644
index 000000000..20327c5b2
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -0,0 +1,461 @@
+/*
+ * 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.keyboard;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.keyboard.internal.KeyboardParser;
+import com.android.inputmethod.keyboard.internal.KeyboardShiftState;
+import com.android.inputmethod.latin.R;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
+ * consists of rows of keys.
+ * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
+ * <pre>
+ * &lt;Keyboard
+ * latin:keyWidth="%10p"
+ * latin:keyHeight="50px"
+ * latin:horizontalGap="2px"
+ * latin:verticalGap="2px" &gt;
+ * &lt;Row latin:keyWidth="32px" &gt;
+ * &lt;Key latin:keyLabel="A" /&gt;
+ * ...
+ * &lt;/Row&gt;
+ * ...
+ * &lt;/Keyboard&gt;
+ * </pre>
+ */
+public class Keyboard {
+ private static final String TAG = Keyboard.class.getSimpleName();
+
+ public static final int EDGE_LEFT = 0x01;
+ public static final int EDGE_RIGHT = 0x02;
+ public static final int EDGE_TOP = 0x04;
+ public static final int EDGE_BOTTOM = 0x08;
+
+ /** Some common keys code. These should be aligned with values/keycodes.xml */
+ public static final int CODE_ENTER = '\n';
+ public static final int CODE_TAB = '\t';
+ public static final int CODE_SPACE = ' ';
+ public static final int CODE_PERIOD = '.';
+ public static final int CODE_DASH = '-';
+ public static final int CODE_SINGLE_QUOTE = '\'';
+ public static final int CODE_DOUBLE_QUOTE = '"';
+
+ /** Special keys code. These should be aligned with values/keycodes.xml */
+ public static final int CODE_DUMMY = 0;
+ public static final int CODE_SHIFT = -1;
+ public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
+ public static final int CODE_CAPSLOCK = -3;
+ public static final int CODE_CANCEL = -4;
+ public static final int CODE_DELETE = -5;
+ public static final int CODE_SETTINGS = -6;
+ public static final int CODE_SETTINGS_LONGPRESS = -7;
+ public static final int CODE_SHORTCUT = -8;
+ // Code value representing the code is not specified.
+ public static final int CODE_UNSPECIFIED = -99;
+
+ /** Horizontal gap default for all rows */
+ private int mDefaultHorizontalGap;
+
+ /** Default key width */
+ private int mDefaultWidth;
+
+ /** Default key height */
+ private int mDefaultHeight;
+
+ /** Default gap between rows */
+ private int mDefaultVerticalGap;
+
+ /** Popup keyboard template */
+ private int mPopupKeyboardResId;
+
+ /** Maximum column for popup keyboard */
+ private int mMaxPopupColumn;
+
+ /** List of shift keys in this keyboard and its icons and state */
+ private final List<Key> mShiftKeys = new ArrayList<Key>();
+ private final HashMap<Key, Drawable> mShiftedIcons = new HashMap<Key, Drawable>();
+ private final HashMap<Key, Drawable> mNormalShiftIcons = new HashMap<Key, Drawable>();
+ private final HashSet<Key> mShiftLockEnabled = new HashSet<Key>();
+ private final KeyboardShiftState mShiftState = new KeyboardShiftState();
+
+ /** Total height of the keyboard, including the padding and keys */
+ private int mTotalHeight;
+
+ /**
+ * Total width (minimum width) of the keyboard, including left side gaps and keys, but not any
+ * gaps on the right side.
+ */
+ private int mMinWidth;
+
+ /** List of keys in this keyboard */
+ private final List<Key> mKeys = new ArrayList<Key>();
+
+ /** Width of the screen available to fit the keyboard */
+ private final int mDisplayWidth;
+
+ /** Height of the screen */
+ private final int mDisplayHeight;
+
+ /** Height of keyboard */
+ private int mKeyboardHeight;
+
+ private int mMostCommonKeyWidth = 0;
+
+ public final KeyboardId mId;
+
+ public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
+
+ // Variables for pre-computing nearest keys.
+
+ // TODO: Change GRID_WIDTH and GRID_HEIGHT to private.
+ public final int GRID_WIDTH;
+ public final int GRID_HEIGHT;
+ private final int GRID_SIZE;
+ private int mCellWidth;
+ private int mCellHeight;
+ private int[][] mGridNeighbors;
+ private int mProximityThreshold;
+ private static int[] EMPTY_INT_ARRAY = new int[0];
+ /** Number of key widths from current touch point to search for nearest keys. */
+ private static float SEARCH_DISTANCE = 1.2f;
+
+ private final ProximityInfo mProximityInfo;
+
+ /**
+ * Creates a keyboard from the given xml key layout file.
+ * @param context the application or service context
+ * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
+ * @param id keyboard identifier
+ * @param width keyboard width
+ */
+
+ public Keyboard(Context context, int xmlLayoutResId, KeyboardId id, int width) {
+ final Resources res = context.getResources();
+ GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
+ GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
+ GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
+
+ final int horizontalEdgesPadding = (int)res.getDimension(
+ R.dimen.keyboard_horizontal_edges_padding);
+ mDisplayWidth = width - horizontalEdgesPadding * 2;
+ // TODO: Adjust the height by referring to the height of area available for drawing as well.
+ mDisplayHeight = res.getDisplayMetrics().heightPixels;
+
+ mDefaultHorizontalGap = 0;
+ setKeyWidth(mDisplayWidth / 10);
+ mDefaultVerticalGap = 0;
+ mDefaultHeight = mDefaultWidth;
+ mId = id;
+ mProximityInfo = new ProximityInfo(GRID_WIDTH, GRID_HEIGHT);
+ loadKeyboard(context, xmlLayoutResId);
+ }
+
+ public int getProximityInfo() {
+ return mProximityInfo.getNativeProximityInfo(this);
+ }
+
+ public List<Key> getKeys() {
+ return mKeys;
+ }
+
+ public int getHorizontalGap() {
+ return mDefaultHorizontalGap;
+ }
+
+ public void setHorizontalGap(int gap) {
+ mDefaultHorizontalGap = gap;
+ }
+
+ public int getVerticalGap() {
+ return mDefaultVerticalGap;
+ }
+
+ public void setVerticalGap(int gap) {
+ mDefaultVerticalGap = gap;
+ }
+
+ public int getRowHeight() {
+ return mDefaultHeight;
+ }
+
+ public void setRowHeight(int height) {
+ mDefaultHeight = height;
+ }
+
+ public int getKeyWidth() {
+ return mDefaultWidth;
+ }
+
+ public void setKeyWidth(int width) {
+ mDefaultWidth = width;
+ final int threshold = (int) (width * SEARCH_DISTANCE);
+ mProximityThreshold = threshold * threshold;
+ }
+
+ /**
+ * Returns the total height of the keyboard
+ * @return the total height of the keyboard
+ */
+ public int getHeight() {
+ return mTotalHeight;
+ }
+
+ public void setHeight(int height) {
+ mTotalHeight = height;
+ }
+
+ public int getMinWidth() {
+ return mMinWidth;
+ }
+
+ public void setMinWidth(int minWidth) {
+ mMinWidth = minWidth;
+ }
+
+ public int getDisplayHeight() {
+ return mDisplayHeight;
+ }
+
+ public int getDisplayWidth() {
+ return mDisplayWidth;
+ }
+
+ public int getKeyboardHeight() {
+ return mKeyboardHeight;
+ }
+
+ public void setKeyboardHeight(int height) {
+ mKeyboardHeight = height;
+ }
+
+ public int getPopupKeyboardResId() {
+ return mPopupKeyboardResId;
+ }
+
+ public void setPopupKeyboardResId(int resId) {
+ mPopupKeyboardResId = resId;
+ }
+
+ public int getMaxPopupKeyboardColumn() {
+ return mMaxPopupColumn;
+ }
+
+ public void setMaxPopupKeyboardColumn(int column) {
+ mMaxPopupColumn = column;
+ }
+
+ public List<Key> getShiftKeys() {
+ return mShiftKeys;
+ }
+
+ public Map<Key, Drawable> getShiftedIcons() {
+ return mShiftedIcons;
+ }
+
+ public void enableShiftLock() {
+ for (final Key key : getShiftKeys()) {
+ mShiftLockEnabled.add(key);
+ mNormalShiftIcons.put(key, key.getIcon());
+ }
+ }
+
+ public boolean isShiftLockEnabled(Key key) {
+ return mShiftLockEnabled.contains(key);
+ }
+
+ public boolean setShiftLocked(boolean newShiftLockState) {
+ final Map<Key, Drawable> shiftedIcons = getShiftedIcons();
+ for (final Key key : getShiftKeys()) {
+ key.setHighlightOn(newShiftLockState);
+ key.setIcon(newShiftLockState ? shiftedIcons.get(key) : mNormalShiftIcons.get(key));
+ }
+ mShiftState.setShiftLocked(newShiftLockState);
+ return true;
+ }
+
+ public boolean isShiftLocked() {
+ return mShiftState.isShiftLocked();
+ }
+
+ public boolean setShifted(boolean newShiftState) {
+ final Map<Key, Drawable> shiftedIcons = getShiftedIcons();
+ for (final Key key : getShiftKeys()) {
+ if (!newShiftState && !mShiftState.isShiftLocked()) {
+ key.setIcon(mNormalShiftIcons.get(key));
+ } else if (newShiftState && !mShiftState.isShiftedOrShiftLocked()) {
+ key.setIcon(shiftedIcons.get(key));
+ }
+ }
+ return mShiftState.setShifted(newShiftState);
+ }
+
+ public boolean isShiftedOrShiftLocked() {
+ return mShiftState.isShiftedOrShiftLocked();
+ }
+
+ public void setAutomaticTemporaryUpperCase() {
+ setShifted(true);
+ mShiftState.setAutomaticTemporaryUpperCase();
+ }
+
+ public boolean isAutomaticTemporaryUpperCase() {
+ return isAlphaKeyboard() && mShiftState.isAutomaticTemporaryUpperCase();
+ }
+
+ public boolean isManualTemporaryUpperCase() {
+ return isAlphaKeyboard() && mShiftState.isManualTemporaryUpperCase();
+ }
+
+ public boolean isManualTemporaryUpperCaseFromAuto() {
+ return isAlphaKeyboard() && mShiftState.isManualTemporaryUpperCaseFromAuto();
+ }
+
+ public KeyboardShiftState getKeyboardShiftState() {
+ return mShiftState;
+ }
+
+ public boolean isAlphaKeyboard() {
+ return mId != null && mId.isAlphabetKeyboard();
+ }
+
+ public boolean isPhoneKeyboard() {
+ return mId != null && mId.isPhoneKeyboard();
+ }
+
+ public boolean isNumberKeyboard() {
+ return mId != null && mId.isNumberKeyboard();
+ }
+
+ // TODO: Move this function to ProximityInfo and make this private.
+ public void computeNearestNeighbors() {
+ // Round-up so we don't have any pixels outside the grid
+ mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
+ mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
+ mGridNeighbors = new int[GRID_SIZE][];
+ final int[] indices = new int[mKeys.size()];
+ final int gridWidth = GRID_WIDTH * mCellWidth;
+ final int gridHeight = GRID_HEIGHT * mCellHeight;
+ final int threshold = mProximityThreshold;
+ for (int x = 0; x < gridWidth; x += mCellWidth) {
+ for (int y = 0; y < gridHeight; y += mCellHeight) {
+ final int centerX = x + mCellWidth / 2;
+ final int centerY = y + mCellHeight / 2;
+ int count = 0;
+ for (int i = 0; i < mKeys.size(); i++) {
+ final Key key = mKeys.get(i);
+ if (key.squaredDistanceToEdge(centerX, centerY) < threshold)
+ indices[count++] = i;
+ }
+ final int[] cell = new int[count];
+ System.arraycopy(indices, 0, cell, 0, count);
+ mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
+ }
+ }
+ mProximityInfo.setProximityInfo(mGridNeighbors, getMinWidth(), getHeight(), mKeys);
+ }
+
+ /**
+ * Returns the indices of the keys that are closest to the given point.
+ * @param x the x-coordinate of the point
+ * @param y the y-coordinate of the point
+ * @return the array of integer indices for the nearest keys to the given point. If the given
+ * point is out of range, then an array of size zero is returned.
+ */
+ public int[] getNearestKeys(int x, int y) {
+ if (mGridNeighbors == null) computeNearestNeighbors();
+ if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
+ int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
+ if (index < GRID_SIZE) {
+ return mGridNeighbors[index];
+ }
+ }
+ return EMPTY_INT_ARRAY;
+ }
+
+ /**
+ * Compute the most common key width in order to use it as proximity key detection threshold.
+ *
+ * @return The most common key width in the keyboard
+ */
+ public int getMostCommonKeyWidth() {
+ if (mMostCommonKeyWidth == 0) {
+ final HashMap<Integer, Integer> histogram = new HashMap<Integer, Integer>();
+ int maxCount = 0;
+ int mostCommonWidth = 0;
+ for (final Key key : mKeys) {
+ final Integer width = key.mWidth + key.mGap;
+ Integer count = histogram.get(width);
+ if (count == null)
+ count = 0;
+ histogram.put(width, ++count);
+ if (count > maxCount) {
+ maxCount = count;
+ mostCommonWidth = width;
+ }
+ }
+ mMostCommonKeyWidth = mostCommonWidth;
+ }
+ return mMostCommonKeyWidth;
+ }
+
+ /**
+ * Return true if spacebar needs showing preview even when "popup on keypress" is off.
+ * @param keyIndex index of the pressing key
+ * @return true if spacebar needs showing preview
+ */
+ public boolean needSpacebarPreview(int keyIndex) {
+ return false;
+ }
+
+ private void loadKeyboard(Context context, int xmlLayoutResId) {
+ try {
+ KeyboardParser parser = new KeyboardParser(this, context);
+ parser.parseKeyboard(xmlLayoutResId);
+ // mMinWidth is the width of this keyboard which is maximum width of row.
+ mMinWidth = parser.getMaxRowWidth();
+ mTotalHeight = parser.getTotalHeight();
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, "keyboard XML parse error: " + e);
+ throw new IllegalArgumentException(e);
+ } catch (IOException e) {
+ Log.w(TAG, "keyboard XML parse error: " + e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void setDefaultBounds(Drawable drawable) {
+ if (drawable != null)
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight());
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
new file mode 100644
index 000000000..7e67d6f6b
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -0,0 +1,78 @@
+/*
+ * 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.keyboard;
+
+public interface KeyboardActionListener {
+
+ /**
+ * Called when the user presses a key. This is sent before the {@link #onCodeInput} is called.
+ * For keys that repeat, this is only called once.
+ *
+ * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
+ * the value will be zero.
+ * @param withSliding true if pressing has occurred because the user slid finger from other key
+ * to this key without releasing the finger.
+ */
+ public void onPress(int primaryCode, boolean withSliding);
+
+ /**
+ * Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
+ * For keys that repeat, this is only called once.
+ *
+ * @param primaryCode the code of the key that was released
+ * @param withSliding true if releasing has occurred because the user slid finger from the key
+ * to other key without releasing the finger.
+ */
+ public void onRelease(int primaryCode, boolean withSliding);
+
+ /**
+ * Send a key code to the listener.
+ *
+ * @param primaryCode this is the code of the key that was pressed
+ * @param keyCodes the codes for all the possible alternative keys with the primary code being
+ * the first. If the primary key code is a single character such as an alphabet or
+ * number or symbol, the alternatives will include other characters that may be on
+ * the same key or adjacent keys. These codes are useful to correct for accidental
+ * presses of a key adjacent to the intended key.
+ * @param x x-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
+ * {@link PointerTracker#onTouchEvent} or so, the value should be
+ * {@link #NOT_A_TOUCH_COORDINATE}.
+ * @param y y-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
+ * {@link PointerTracker#onTouchEvent} or so, the value should be
+ * {@link #NOT_A_TOUCH_COORDINATE}.
+ */
+ public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y);
+
+ public static final int NOT_A_TOUCH_COORDINATE = -1;
+
+ /**
+ * Sends a sequence of characters to the listener.
+ *
+ * @param text the sequence of characters to be displayed.
+ */
+ public void onTextInput(CharSequence text);
+
+ /**
+ * Called when user released a finger outside any key.
+ */
+ public void onCancelInput();
+
+ /**
+ * Called when the user quickly moves the finger from up to down.
+ */
+ public void onSwipeDown();
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
new file mode 100644
index 000000000..9c63c198c
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -0,0 +1,220 @@
+/*
+ * 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.keyboard;
+
+import android.view.inputmethod.EditorInfo;
+
+import com.android.inputmethod.compat.EditorInfoCompatUtils;
+import com.android.inputmethod.compat.InputTypeCompatUtils;
+import com.android.inputmethod.latin.R;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Represents the parameters necessary to construct a new LatinKeyboard,
+ * which also serve as a unique identifier for each keyboard type.
+ */
+public class KeyboardId {
+ public static final int MODE_TEXT = 0;
+ public static final int MODE_URL = 1;
+ public static final int MODE_EMAIL = 2;
+ public static final int MODE_IM = 3;
+ public static final int MODE_PHONE = 4;
+ public static final int MODE_NUMBER = 5;
+
+ public static final int F2KEY_MODE_NONE = 0;
+ public static final int F2KEY_MODE_SETTINGS = 1;
+ public static final int F2KEY_MODE_SHORTCUT_IME = 2;
+ public static final int F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS = 3;
+
+ public final Locale mLocale;
+ public final int mOrientation;
+ public final int mWidth;
+ public final int mMode;
+ public final int mXmlId;
+ public final boolean mNavigateAction;
+ public final boolean mPasswordInput;
+ // TODO: Clean up these booleans and modes.
+ public final boolean mHasSettingsKey;
+ public final int mF2KeyMode;
+ public final boolean mClobberSettingsKey;
+ public final boolean mVoiceKeyEnabled;
+ public final boolean mHasVoiceKey;
+ public final int mImeAction;
+ public final boolean mEnableShiftLock;
+
+ public final String mXmlName;
+ public final EditorInfo mAttribute;
+
+ private final int mHashCode;
+
+ public KeyboardId(String xmlName, int xmlId, Locale locale, int orientation, int width,
+ int mode, EditorInfo attribute, boolean hasSettingsKey, int f2KeyMode,
+ boolean clobberSettingsKey, boolean voiceKeyEnabled, boolean hasVoiceKey,
+ boolean enableShiftLock) {
+ final int inputType = (attribute != null) ? attribute.inputType : 0;
+ final int imeOptions = (attribute != null) ? attribute.imeOptions : 0;
+ this.mLocale = locale;
+ this.mOrientation = orientation;
+ this.mWidth = width;
+ this.mMode = mode;
+ this.mXmlId = xmlId;
+ // Note: Turn off checking navigation flag to show TAB key for now.
+ this.mNavigateAction = InputTypeCompatUtils.isWebInputType(inputType);
+// || EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
+// || EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions);
+ this.mPasswordInput = InputTypeCompatUtils.isPasswordInputType(inputType)
+ || InputTypeCompatUtils.isVisiblePasswordInputType(inputType);
+ this.mHasSettingsKey = hasSettingsKey;
+ this.mF2KeyMode = f2KeyMode;
+ this.mClobberSettingsKey = clobberSettingsKey;
+ this.mVoiceKeyEnabled = voiceKeyEnabled;
+ this.mHasVoiceKey = hasVoiceKey;
+ // We are interested only in {@link EditorInfo#IME_MASK_ACTION} enum value and
+ // {@link EditorInfo#IME_FLAG_NO_ENTER_ACTION}.
+ this.mImeAction = imeOptions & (
+ EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION);
+ this.mEnableShiftLock = enableShiftLock;
+
+ this.mXmlName = xmlName;
+ this.mAttribute = attribute;
+
+ this.mHashCode = Arrays.hashCode(new Object[] {
+ locale,
+ orientation,
+ width,
+ mode,
+ xmlId,
+ mNavigateAction,
+ mPasswordInput,
+ hasSettingsKey,
+ f2KeyMode,
+ clobberSettingsKey,
+ voiceKeyEnabled,
+ hasVoiceKey,
+ mImeAction,
+ enableShiftLock,
+ });
+ }
+
+ public KeyboardId cloneWithNewLayout(String xmlName, int xmlId) {
+ return new KeyboardId(xmlName, xmlId, mLocale, mOrientation, mWidth, mMode, mAttribute,
+ mHasSettingsKey, mF2KeyMode, mClobberSettingsKey, mVoiceKeyEnabled, mHasVoiceKey,
+ mEnableShiftLock);
+ }
+
+ public KeyboardId cloneWithNewGeometry(int width) {
+ if (mWidth == width)
+ return this;
+ return new KeyboardId(mXmlName, mXmlId, mLocale, mOrientation, width, mMode, mAttribute,
+ mHasSettingsKey, mF2KeyMode, mClobberSettingsKey, mVoiceKeyEnabled, mHasVoiceKey,
+ mEnableShiftLock);
+ }
+
+ public int getXmlId() {
+ return mXmlId;
+ }
+
+ public boolean isAlphabetKeyboard() {
+ return mXmlId == R.xml.kbd_qwerty;
+ }
+
+ public boolean isSymbolsKeyboard() {
+ return mXmlId == R.xml.kbd_symbols || mXmlId == R.xml.kbd_symbols_shift;
+ }
+
+ public boolean isPhoneKeyboard() {
+ return mMode == MODE_PHONE;
+ }
+
+ public boolean isPhoneSymbolsKeyboard() {
+ return mXmlId == R.xml.kbd_phone_symbols;
+ }
+
+ public boolean isNumberKeyboard() {
+ return mMode == MODE_NUMBER;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof KeyboardId && equals((KeyboardId) other);
+ }
+
+ boolean equals(KeyboardId other) {
+ return other.mLocale.equals(this.mLocale)
+ && other.mOrientation == this.mOrientation
+ && other.mWidth == this.mWidth
+ && other.mMode == this.mMode
+ && other.mXmlId == this.mXmlId
+ && other.mNavigateAction == this.mNavigateAction
+ && other.mPasswordInput == this.mPasswordInput
+ && other.mHasSettingsKey == this.mHasSettingsKey
+ && other.mF2KeyMode == this.mF2KeyMode
+ && other.mClobberSettingsKey == this.mClobberSettingsKey
+ && other.mVoiceKeyEnabled == this.mVoiceKeyEnabled
+ && other.mHasVoiceKey == this.mHasVoiceKey
+ && other.mImeAction == this.mImeAction
+ && other.mEnableShiftLock == this.mEnableShiftLock;
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s.xml %s %s%d %s %s %s%s%s%s%s%s%s%s]",
+ mXmlName,
+ mLocale,
+ (mOrientation == 1 ? "port" : "land"), mWidth,
+ modeName(mMode),
+ EditorInfoCompatUtils.imeOptionsName(mImeAction),
+ f2KeyModeName(mF2KeyMode),
+ (mClobberSettingsKey ? " clobberSettingsKey" : ""),
+ (mNavigateAction ? " navigateAction" : ""),
+ (mPasswordInput ? " passwordInput" : ""),
+ (mHasSettingsKey ? " hasSettingsKey" : ""),
+ (mVoiceKeyEnabled ? " voiceKeyEnabled" : ""),
+ (mHasVoiceKey ? " hasVoiceKey" : ""),
+ (mEnableShiftLock ? " enableShiftLock" : "")
+ );
+ }
+
+ public static String modeName(int mode) {
+ switch (mode) {
+ case MODE_TEXT: return "text";
+ case MODE_URL: return "url";
+ case MODE_EMAIL: return "email";
+ case MODE_IM: return "im";
+ case MODE_PHONE: return "phone";
+ case MODE_NUMBER: return "number";
+ default: return null;
+ }
+ }
+
+ public static String f2KeyModeName(int f2KeyMode) {
+ switch (f2KeyMode) {
+ case F2KEY_MODE_NONE: return "none";
+ case F2KEY_MODE_SETTINGS: return "settings";
+ case F2KEY_MODE_SHORTCUT_IME: return "shortcutIme";
+ case F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS: return "shortcutImeOrSettings";
+ default: return null;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
new file mode 100644
index 000000000..275e9d1fe
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -0,0 +1,849 @@
+/*
+ * 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.keyboard;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
+import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.keyboard.internal.ModifierKeyState;
+import com.android.inputmethod.keyboard.internal.ShiftKeyState;
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.Settings;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.Utils;
+
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+import java.util.Locale;
+
+public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = KeyboardSwitcher.class.getSimpleName();
+ private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
+ public static final boolean DEBUG_STATE = false;
+
+ public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20100902";
+ private static final int[] KEYBOARD_THEMES = {
+ R.style.KeyboardTheme,
+ R.style.KeyboardTheme_HighContrast,
+ R.style.KeyboardTheme_Stone,
+ R.style.KeyboardTheme_Stone_Bold,
+ R.style.KeyboardTheme_Gingerbread,
+ R.style.KeyboardTheme_IceCreamSandwich,
+ };
+
+ private SubtypeSwitcher mSubtypeSwitcher;
+ private SharedPreferences mPrefs;
+
+ private View mCurrentInputView;
+ private LatinKeyboardView mKeyboardView;
+ private LatinIME mInputMethodService;
+
+ // TODO: Combine these key state objects with auto mode switch state.
+ private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
+ private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
+
+ private KeyboardId mSymbolsId;
+ private KeyboardId mSymbolsShiftedId;
+
+ private KeyboardId mCurrentId;
+ private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboardCache =
+ new HashMap<KeyboardId, SoftReference<LatinKeyboard>>();
+
+ private EditorInfo mAttribute;
+ private boolean mIsSymbols;
+ /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of
+ * what user actually typed. */
+ private boolean mIsAutoCorrectionActive;
+ private boolean mVoiceKeyEnabled;
+ private boolean mVoiceButtonOnPrimary;
+
+ // TODO: Encapsulate these state handling to separate class and combine with ShiftKeyState
+ // and ModifierKeyState.
+ private static final int SWITCH_STATE_ALPHA = 0;
+ private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
+ private static final int SWITCH_STATE_SYMBOL = 2;
+ // The following states are used only on the distinct multi-touch panel devices.
+ private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
+ private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
+ private static final int SWITCH_STATE_CHORDING_ALPHA = 5;
+ private static final int SWITCH_STATE_CHORDING_SYMBOL = 6;
+ private int mSwitchState = SWITCH_STATE_ALPHA;
+
+ // Indicates whether or not we have the settings key in option of settings
+ private boolean mSettingsKeyEnabledInSettings;
+ private static final int SETTINGS_KEY_MODE_AUTO = R.string.settings_key_mode_auto;
+ private static final int SETTINGS_KEY_MODE_ALWAYS_SHOW =
+ R.string.settings_key_mode_always_show;
+ // NOTE: No need to have SETTINGS_KEY_MODE_ALWAYS_HIDE here because it's not being referred to
+ // in the source code now.
+ // Default is SETTINGS_KEY_MODE_AUTO.
+ private static final int DEFAULT_SETTINGS_KEY_MODE = SETTINGS_KEY_MODE_AUTO;
+
+ private int mThemeIndex = -1;
+ private Context mThemeContext;
+ private int mKeyboardWidth;
+
+ private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
+
+ public static KeyboardSwitcher getInstance() {
+ return sInstance;
+ }
+
+ private KeyboardSwitcher() {
+ // Intentional empty constructor for singleton.
+ }
+
+ public static void init(LatinIME ims, SharedPreferences prefs) {
+ sInstance.mInputMethodService = ims;
+ sInstance.mPrefs = prefs;
+ sInstance.mSubtypeSwitcher = SubtypeSwitcher.getInstance();
+ sInstance.setContextThemeWrapper(ims, getKeyboardThemeIndex(ims, prefs));
+ prefs.registerOnSharedPreferenceChangeListener(sInstance);
+ }
+
+ private static int getKeyboardThemeIndex(Context context, SharedPreferences prefs) {
+ final String defaultThemeId = context.getString(R.string.config_default_keyboard_theme_id);
+ final String themeId = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeId);
+ try {
+ final int themeIndex = Integer.valueOf(themeId);
+ if (themeIndex >= 0 && themeIndex < KEYBOARD_THEMES.length)
+ return themeIndex;
+ } catch (NumberFormatException e) {
+ // Format error, keyboard theme is default to 0.
+ }
+ Log.w(TAG, "Illegal keyboard theme in preference: " + themeId + ", default to 0");
+ return 0;
+ }
+
+ private void setContextThemeWrapper(Context context, int themeIndex) {
+ if (mThemeIndex != themeIndex) {
+ mThemeIndex = themeIndex;
+ mThemeContext = new ContextThemeWrapper(context, KEYBOARD_THEMES[themeIndex]);
+ mKeyboardCache.clear();
+ }
+ }
+
+ public void loadKeyboard(EditorInfo attribute, boolean voiceKeyEnabled,
+ boolean voiceButtonOnPrimary) {
+ mSwitchState = SWITCH_STATE_ALPHA;
+ try {
+ loadKeyboardInternal(attribute, voiceKeyEnabled, voiceButtonOnPrimary, false);
+ } catch (RuntimeException e) {
+ // Get KeyboardId to record which keyboard has been failed to load.
+ final KeyboardId id = getKeyboardId(attribute, false);
+ Log.w(TAG, "loading keyboard failed: " + id, e);
+ LatinImeLogger.logOnException(id.toString(), e);
+ }
+ }
+
+ private void loadKeyboardInternal(EditorInfo attribute, boolean voiceButtonEnabled,
+ boolean voiceButtonOnPrimary, boolean isSymbols) {
+ if (mKeyboardView == null) return;
+
+ mAttribute = attribute;
+ mVoiceKeyEnabled = voiceButtonEnabled;
+ mVoiceButtonOnPrimary = voiceButtonOnPrimary;
+ mIsSymbols = isSymbols;
+ // Update the settings key state because number of enabled IMEs could have been changed
+ mSettingsKeyEnabledInSettings = getSettingsKeyMode(mPrefs, mInputMethodService);
+ final KeyboardId id = getKeyboardId(attribute, isSymbols);
+
+ // Note: This comment is only applied for phone number keyboard layout.
+ // On non-xlarge device, "@integer/key_switch_alpha_symbol" key code is used to switch
+ // between "phone keyboard" and "phone symbols keyboard". But on xlarge device,
+ // "@integer/key_shift" key code is used for that purpose in order to properly display
+ // "more" and "locked more" key labels. To achieve these behavior, we should initialize
+ // mSymbolsId and mSymbolsShiftedId to "phone keyboard" and "phone symbols keyboard"
+ // respectively here for xlarge device's layout switching.
+ mSymbolsId = makeSiblingKeyboardId(id, R.xml.kbd_symbols, R.xml.kbd_phone);
+ mSymbolsShiftedId = makeSiblingKeyboardId(
+ id, R.xml.kbd_symbols_shift, R.xml.kbd_phone_symbols);
+
+ setKeyboard(getKeyboard(id));
+ }
+
+ public void onSizeChanged() {
+ final int width = mInputMethodService.getWindow().getWindow().getDecorView().getWidth();
+ if (width == 0 || mCurrentId == null)
+ return;
+ mKeyboardWidth = width;
+ // Set keyboard with new width.
+ final KeyboardId newId = mCurrentId.cloneWithNewGeometry(width);
+ setKeyboard(getKeyboard(newId));
+ }
+
+ private void setKeyboard(final Keyboard newKeyboard) {
+ final Keyboard oldKeyboard = mKeyboardView.getKeyboard();
+ mKeyboardView.setKeyboard(newKeyboard);
+ mCurrentId = newKeyboard.mId;
+ final Resources res = mInputMethodService.getResources();
+ mKeyboardView.setKeyPreviewPopupEnabled(
+ Settings.Values.isKeyPreviewPopupEnabled(mPrefs, res),
+ Settings.Values.getKeyPreviewPopupDismissDelay(mPrefs, res));
+ final boolean localeChanged = (oldKeyboard == null)
+ || !newKeyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale);
+ mInputMethodService.mHandler.startDisplayLanguageOnSpacebar(localeChanged);
+ }
+
+ private LatinKeyboard getKeyboard(KeyboardId id) {
+ final SoftReference<LatinKeyboard> ref = mKeyboardCache.get(id);
+ LatinKeyboard keyboard = (ref == null) ? null : ref.get();
+ if (keyboard == null) {
+ final Resources res = mInputMethodService.getResources();
+ final Locale savedLocale = Utils.setSystemLocale(res,
+ mSubtypeSwitcher.getInputLocale());
+
+ keyboard = new LatinKeyboard(mThemeContext, id, id.mWidth);
+
+ if (id.mEnableShiftLock) {
+ keyboard.enableShiftLock();
+ }
+
+ mKeyboardCache.put(id, new SoftReference<LatinKeyboard>(keyboard));
+ if (DEBUG_CACHE)
+ Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": "
+ + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
+
+ Utils.setSystemLocale(res, savedLocale);
+ } else if (DEBUG_CACHE) {
+ Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": HIT id=" + id);
+ }
+
+ keyboard.onAutoCorrectionStateChanged(mIsAutoCorrectionActive);
+ keyboard.setShifted(false);
+ // If the cached keyboard had been switched to another keyboard while the language was
+ // displayed on its spacebar, it might have had arbitrary text fade factor. In such case,
+ // we should reset the text fade factor. It is also applicable to shortcut key.
+ keyboard.setSpacebarTextFadeFactor(0.0f, null);
+ keyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady(), null);
+ keyboard.setSpacebarSlidingLanguageSwitchDiff(0);
+ return keyboard;
+ }
+
+ private boolean hasVoiceKey(boolean isSymbols) {
+ return mVoiceKeyEnabled && (isSymbols != mVoiceButtonOnPrimary);
+ }
+
+ private boolean hasSettingsKey(EditorInfo attribute) {
+ return mSettingsKeyEnabledInSettings
+ && !Utils.inPrivateImeOptions(mInputMethodService.getPackageName(),
+ LatinIME.IME_OPTION_NO_SETTINGS_KEY, attribute);
+ }
+
+ private KeyboardId getKeyboardId(EditorInfo attribute, boolean isSymbols) {
+ final int mode = Utils.getKeyboardMode(attribute);
+ final boolean hasVoiceKey = hasVoiceKey(isSymbols);
+ final int xmlId;
+ final boolean enableShiftLock;
+
+ if (isSymbols) {
+ if (mode == KeyboardId.MODE_PHONE) {
+ xmlId = R.xml.kbd_phone_symbols;
+ } else if (mode == KeyboardId.MODE_NUMBER) {
+ // Note: MODE_NUMBER keyboard layout has no "switch alpha symbol" key.
+ xmlId = R.xml.kbd_number;
+ } else {
+ xmlId = R.xml.kbd_symbols;
+ }
+ enableShiftLock = false;
+ } else {
+ if (mode == KeyboardId.MODE_PHONE) {
+ xmlId = R.xml.kbd_phone;
+ enableShiftLock = false;
+ } else if (mode == KeyboardId.MODE_NUMBER) {
+ xmlId = R.xml.kbd_number;
+ enableShiftLock = false;
+ } else {
+ xmlId = R.xml.kbd_qwerty;
+ enableShiftLock = true;
+ }
+ }
+ final boolean hasSettingsKey = hasSettingsKey(attribute);
+ final int f2KeyMode = getF2KeyMode(mPrefs, mInputMethodService, attribute);
+ final boolean clobberSettingsKey = Utils.inPrivateImeOptions(
+ mInputMethodService.getPackageName(), LatinIME.IME_OPTION_NO_SETTINGS_KEY,
+ attribute);
+ final Resources res = mInputMethodService.getResources();
+ final int orientation = res.getConfiguration().orientation;
+ if (mKeyboardWidth == 0)
+ mKeyboardWidth = res.getDisplayMetrics().widthPixels;
+ final Locale locale = mSubtypeSwitcher.getInputLocale();
+ return new KeyboardId(
+ res.getResourceEntryName(xmlId), xmlId, locale, orientation, mKeyboardWidth,
+ mode, attribute, hasSettingsKey, f2KeyMode, clobberSettingsKey, mVoiceKeyEnabled,
+ hasVoiceKey, enableShiftLock);
+ }
+
+ private KeyboardId makeSiblingKeyboardId(KeyboardId base, int alphabet, int phone) {
+ final int xmlId = base.mMode == KeyboardId.MODE_PHONE ? phone : alphabet;
+ final String xmlName = mInputMethodService.getResources().getResourceEntryName(xmlId);
+ return base.cloneWithNewLayout(xmlName, xmlId);
+ }
+
+ public int getKeyboardMode() {
+ return mCurrentId != null ? mCurrentId.mMode : KeyboardId.MODE_TEXT;
+ }
+
+ public boolean isAlphabetMode() {
+ return mCurrentId != null && mCurrentId.isAlphabetKeyboard();
+ }
+
+ public boolean isInputViewShown() {
+ return mCurrentInputView != null && mCurrentInputView.isShown();
+ }
+
+ public boolean isKeyboardAvailable() {
+ if (mKeyboardView != null)
+ return mKeyboardView.getKeyboard() != null;
+ return false;
+ }
+
+ public LatinKeyboard getLatinKeyboard() {
+ if (mKeyboardView != null) {
+ final Keyboard keyboard = mKeyboardView.getKeyboard();
+ if (keyboard instanceof LatinKeyboard)
+ return (LatinKeyboard)keyboard;
+ }
+ return null;
+ }
+
+ public boolean isShiftedOrShiftLocked() {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null)
+ return latinKeyboard.isShiftedOrShiftLocked();
+ return false;
+ }
+
+ public boolean isShiftLocked() {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null)
+ return latinKeyboard.isShiftLocked();
+ return false;
+ }
+
+ public boolean isAutomaticTemporaryUpperCase() {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null)
+ return latinKeyboard.isAutomaticTemporaryUpperCase();
+ return false;
+ }
+
+ public boolean isManualTemporaryUpperCase() {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null)
+ return latinKeyboard.isManualTemporaryUpperCase();
+ return false;
+ }
+
+ private boolean isManualTemporaryUpperCaseFromAuto() {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null)
+ return latinKeyboard.isManualTemporaryUpperCaseFromAuto();
+ return false;
+ }
+
+ private void setManualTemporaryUpperCase(boolean shifted) {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null) {
+ // On non-distinct multi touch panel device, we should also turn off the shift locked
+ // state when shift key is pressed to go to normal mode.
+ // On the other hand, on distinct multi touch panel device, turning off the shift locked
+ // state with shift key pressing is handled by onReleaseShift().
+ if (!hasDistinctMultitouch() && !shifted && latinKeyboard.isShiftLocked()) {
+ latinKeyboard.setShiftLocked(false);
+ }
+ if (latinKeyboard.setShifted(shifted)) {
+ mKeyboardView.invalidateAllKeys();
+ }
+ }
+ }
+
+ private void setShiftLocked(boolean shiftLocked) {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null && latinKeyboard.setShiftLocked(shiftLocked)) {
+ mKeyboardView.invalidateAllKeys();
+ }
+ }
+
+ /**
+ * Toggle keyboard shift state triggered by user touch event.
+ */
+ public void toggleShift() {
+ mInputMethodService.mHandler.cancelUpdateShiftState();
+ if (DEBUG_STATE)
+ Log.d(TAG, "toggleShift:"
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " shiftKeyState=" + mShiftKeyState);
+ if (isAlphabetMode()) {
+ setManualTemporaryUpperCase(!isShiftedOrShiftLocked());
+ } else {
+ toggleShiftInSymbol();
+ }
+ }
+
+ public void toggleCapsLock() {
+ mInputMethodService.mHandler.cancelUpdateShiftState();
+ if (DEBUG_STATE)
+ Log.d(TAG, "toggleCapsLock:"
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " shiftKeyState=" + mShiftKeyState);
+ if (isAlphabetMode()) {
+ if (isShiftLocked()) {
+ // Shift key is long pressed while caps lock state, we will toggle back to normal
+ // state. And mark as if shift key is released.
+ setShiftLocked(false);
+ mShiftKeyState.onRelease();
+ } else {
+ setShiftLocked(true);
+ }
+ }
+ }
+
+ private void setAutomaticTemporaryUpperCase() {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null) {
+ latinKeyboard.setAutomaticTemporaryUpperCase();
+ mKeyboardView.invalidateAllKeys();
+ }
+ }
+
+ /**
+ * Update keyboard shift state triggered by connected EditText status change.
+ */
+ public void updateShiftState() {
+ final ShiftKeyState shiftKeyState = mShiftKeyState;
+ if (DEBUG_STATE)
+ Log.d(TAG, "updateShiftState:"
+ + " autoCaps=" + mInputMethodService.getCurrentAutoCapsState()
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " shiftKeyState=" + shiftKeyState);
+ if (isAlphabetMode()) {
+ if (!isShiftLocked() && !shiftKeyState.isIgnoring()) {
+ if (shiftKeyState.isReleasing() && mInputMethodService.getCurrentAutoCapsState()) {
+ // Only when shift key is releasing, automatic temporary upper case will be set.
+ setAutomaticTemporaryUpperCase();
+ } else {
+ setManualTemporaryUpperCase(shiftKeyState.isMomentary());
+ }
+ }
+ } else {
+ // In symbol keyboard mode, we should clear shift key state because only alphabet
+ // keyboard has shift key.
+ shiftKeyState.onRelease();
+ }
+ }
+
+ public void changeKeyboardMode() {
+ if (DEBUG_STATE)
+ Log.d(TAG, "changeKeyboardMode:"
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " shiftKeyState=" + mShiftKeyState);
+ toggleKeyboardMode();
+ if (isShiftLocked() && isAlphabetMode())
+ setShiftLocked(true);
+ updateShiftState();
+ }
+
+ public void onPressShift(boolean withSliding) {
+ if (!isKeyboardAvailable())
+ return;
+ ShiftKeyState shiftKeyState = mShiftKeyState;
+ if (DEBUG_STATE)
+ Log.d(TAG, "onPressShift:"
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " shiftKeyState=" + shiftKeyState + " sliding=" + withSliding);
+ if (isAlphabetMode()) {
+ if (isShiftLocked()) {
+ // Shift key is pressed while caps lock state, we will treat this state as shifted
+ // caps lock state and mark as if shift key pressed while normal state.
+ shiftKeyState.onPress();
+ setManualTemporaryUpperCase(true);
+ } else if (isAutomaticTemporaryUpperCase()) {
+ // Shift key is pressed while automatic temporary upper case, we have to move to
+ // manual temporary upper case.
+ shiftKeyState.onPress();
+ setManualTemporaryUpperCase(true);
+ } else if (isShiftedOrShiftLocked()) {
+ // In manual upper case state, we just record shift key has been pressing while
+ // shifted state.
+ shiftKeyState.onPressOnShifted();
+ } else {
+ // In base layout, chording or manual temporary upper case mode is started.
+ shiftKeyState.onPress();
+ toggleShift();
+ }
+ } else {
+ // In symbol mode, just toggle symbol and symbol more keyboard.
+ shiftKeyState.onPress();
+ toggleShift();
+ mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
+ }
+ }
+
+ public void onReleaseShift(boolean withSliding) {
+ if (!isKeyboardAvailable())
+ return;
+ ShiftKeyState shiftKeyState = mShiftKeyState;
+ if (DEBUG_STATE)
+ Log.d(TAG, "onReleaseShift:"
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " shiftKeyState=" + shiftKeyState + " sliding=" + withSliding);
+ if (isAlphabetMode()) {
+ if (shiftKeyState.isMomentary()) {
+ // After chording input while normal state.
+ toggleShift();
+ } else if (isShiftLocked() && !shiftKeyState.isIgnoring() && !withSliding) {
+ // Shift has been pressed without chording while caps lock state.
+ toggleCapsLock();
+ // To be able to turn off caps lock by "double tap" on shift key, we should ignore
+ // the second tap of the "double tap" from now for a while because we just have
+ // already turned off caps lock above.
+ mKeyboardView.startIgnoringDoubleTap();
+ } else if (isShiftedOrShiftLocked() && shiftKeyState.isPressingOnShifted()
+ && !withSliding) {
+ // Shift has been pressed without chording while shifted state.
+ toggleShift();
+ } else if (isManualTemporaryUpperCaseFromAuto() && shiftKeyState.isPressing()
+ && !withSliding) {
+ // Shift has been pressed without chording while manual temporary upper case
+ // transited from automatic temporary upper case.
+ toggleShift();
+ }
+ } else {
+ // In symbol mode, snap back to the previous keyboard mode if the user chords the shift
+ // key and another key, then releases the shift key.
+ if (mSwitchState == SWITCH_STATE_CHORDING_SYMBOL) {
+ toggleShift();
+ }
+ }
+ shiftKeyState.onRelease();
+ }
+
+ public void onPressSymbol() {
+ if (DEBUG_STATE)
+ Log.d(TAG, "onPressSymbol:"
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " symbolKeyState=" + mSymbolKeyState);
+ changeKeyboardMode();
+ mSymbolKeyState.onPress();
+ mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
+ }
+
+ public void onReleaseSymbol() {
+ if (DEBUG_STATE)
+ Log.d(TAG, "onReleaseSymbol:"
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " symbolKeyState=" + mSymbolKeyState);
+ // Snap back to the previous keyboard mode if the user chords the mode change key and
+ // another key, then releases the mode change key.
+ if (mSwitchState == SWITCH_STATE_CHORDING_ALPHA) {
+ changeKeyboardMode();
+ }
+ mSymbolKeyState.onRelease();
+ }
+
+ public void onOtherKeyPressed() {
+ if (DEBUG_STATE)
+ Log.d(TAG, "onOtherKeyPressed:"
+ + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+ + " shiftKeyState=" + mShiftKeyState
+ + " symbolKeyState=" + mSymbolKeyState);
+ mShiftKeyState.onOtherKeyPressed();
+ mSymbolKeyState.onOtherKeyPressed();
+ }
+
+ public void onCancelInput() {
+ // Snap back to the previous keyboard mode if the user cancels sliding input.
+ if (getPointerCount() == 1) {
+ if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
+ changeKeyboardMode();
+ } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
+ toggleShift();
+ }
+ }
+ }
+
+ private void toggleShiftInSymbol() {
+ if (isAlphabetMode())
+ return;
+ final LatinKeyboard keyboard;
+ if (mCurrentId.equals(mSymbolsId) || !mCurrentId.equals(mSymbolsShiftedId)) {
+ keyboard = getKeyboard(mSymbolsShiftedId);
+ // Symbol shifted keyboard has an ALT key that has a caps lock style indicator. To
+ // enable the indicator, we need to call setShiftLocked(true).
+ keyboard.setShiftLocked(true);
+ } else {
+ keyboard = getKeyboard(mSymbolsId);
+ // Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the
+ // indicator, we need to call setShiftLocked(false).
+ keyboard.setShiftLocked(false);
+ }
+ setKeyboard(keyboard);
+ }
+
+ public boolean isInMomentarySwitchState() {
+ return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
+ || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
+ }
+
+ public boolean isVibrateAndSoundFeedbackRequired() {
+ return mKeyboardView == null || !mKeyboardView.isInSlidingKeyInput();
+ }
+
+ private int getPointerCount() {
+ return mKeyboardView == null ? 0 : mKeyboardView.getPointerCount();
+ }
+
+ private void toggleKeyboardMode() {
+ loadKeyboardInternal(mAttribute, mVoiceKeyEnabled, mVoiceButtonOnPrimary, !mIsSymbols);
+ if (mIsSymbols) {
+ mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
+ } else {
+ mSwitchState = SWITCH_STATE_ALPHA;
+ }
+ }
+
+ public boolean hasDistinctMultitouch() {
+ return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch();
+ }
+
+ private static boolean isSpaceCharacter(int c) {
+ return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER;
+ }
+
+ private static boolean isQuoteCharacter(int c) {
+ // Apostrophe, quotation mark.
+ if (c == Keyboard.CODE_SINGLE_QUOTE || c == Keyboard.CODE_DOUBLE_QUOTE)
+ return true;
+ // \u2018: Left single quotation mark
+ // \u2019: Right single quotation mark
+ // \u201a: Single low-9 quotation mark
+ // \u201b: Single high-reversed-9 quotation mark
+ // \u201c: Left double quotation mark
+ // \u201d: Right double quotation mark
+ // \u201e: Double low-9 quotation mark
+ // \u201f: Double high-reversed-9 quotation mark
+ if (c >= '\u2018' && c <= '\u201f')
+ return true;
+ // \u00ab: Left-pointing double angle quotation mark
+ // \u00bb: Right-pointing double angle quotation mark
+ if (c == '\u00ab' || c == '\u00bb')
+ return true;
+ return false;
+ }
+
+ /**
+ * Updates state machine to figure out when to automatically snap back to the previous mode.
+ */
+ public void onKey(int code) {
+ if (DEBUG_STATE)
+ Log.d(TAG, "onKey: code=" + code + " switchState=" + mSwitchState
+ + " pointers=" + getPointerCount());
+ switch (mSwitchState) {
+ case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
+ // Only distinct multi touch devices can be in this state.
+ // On non-distinct multi touch devices, mode change key is handled by
+ // {@link LatinIME#onCodeInput}, not by {@link LatinIME#onPress} and
+ // {@link LatinIME#onRelease}. So, on such devices, {@link #mSwitchState} starts
+ // from {@link #SWITCH_STATE_SYMBOL_BEGIN}, or {@link #SWITCH_STATE_ALPHA}, not from
+ // {@link #SWITCH_STATE_MOMENTARY}.
+ if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
+ // Detected only the mode change key has been pressed, and then released.
+ if (mIsSymbols) {
+ mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
+ } else {
+ mSwitchState = SWITCH_STATE_ALPHA;
+ }
+ } else if (getPointerCount() == 1) {
+ // Snap back to the previous keyboard mode if the user pressed the mode change key
+ // and slid to other key, then released the finger.
+ // If the user cancels the sliding input, snapping back to the previous keyboard
+ // mode is handled by {@link #onCancelInput}.
+ changeKeyboardMode();
+ } else {
+ // Chording input is being started. The keyboard mode will be snapped back to the
+ // previous mode in {@link onReleaseSymbol} when the mode change key is released.
+ mSwitchState = SWITCH_STATE_CHORDING_ALPHA;
+ }
+ break;
+ case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
+ if (code == Keyboard.CODE_SHIFT) {
+ // Detected only the shift key has been pressed on symbol layout, and then released.
+ mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
+ } else if (getPointerCount() == 1) {
+ // Snap back to the previous keyboard mode if the user pressed the shift key on
+ // symbol mode and slid to other key, then released the finger.
+ toggleShift();
+ mSwitchState = SWITCH_STATE_SYMBOL;
+ } else {
+ // Chording input is being started. The keyboard mode will be snapped back to the
+ // previous mode in {@link onReleaseShift} when the shift key is released.
+ mSwitchState = SWITCH_STATE_CHORDING_SYMBOL;
+ }
+ break;
+ case SWITCH_STATE_SYMBOL_BEGIN:
+ if (!isSpaceCharacter(code) && code >= 0) {
+ mSwitchState = SWITCH_STATE_SYMBOL;
+ }
+ // Snap back to alpha keyboard mode immediately if user types a quote character.
+ if (isQuoteCharacter(code)) {
+ changeKeyboardMode();
+ }
+ break;
+ case SWITCH_STATE_SYMBOL:
+ case SWITCH_STATE_CHORDING_SYMBOL:
+ // Snap back to alpha keyboard mode if user types one or more non-space/enter
+ // characters followed by a space/enter or a quote character.
+ if (isSpaceCharacter(code) || isQuoteCharacter(code)) {
+ changeKeyboardMode();
+ }
+ break;
+ }
+ }
+
+ public LatinKeyboardView getKeyboardView() {
+ return mKeyboardView;
+ }
+
+ public View onCreateInputView() {
+ return createInputView(mThemeIndex, true);
+ }
+
+ private View createInputView(final int newThemeIndex, final boolean forceRecreate) {
+ if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate)
+ return mCurrentInputView;
+
+ if (mKeyboardView != null) {
+ mKeyboardView.closing();
+ }
+
+ final int oldThemeIndex = mThemeIndex;
+ Utils.GCUtils.getInstance().reset();
+ boolean tryGC = true;
+ for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
+ try {
+ setContextThemeWrapper(mInputMethodService, newThemeIndex);
+ mCurrentInputView = LayoutInflater.from(mThemeContext).inflate(
+ R.layout.input_view, null);
+ tryGC = false;
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "load keyboard failed: " + e);
+ tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
+ oldThemeIndex + "," + newThemeIndex, e);
+ } catch (InflateException e) {
+ Log.w(TAG, "load keyboard failed: " + e);
+ tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
+ oldThemeIndex + "," + newThemeIndex, e);
+ }
+ }
+
+ mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
+ mKeyboardView.setOnKeyboardActionListener(mInputMethodService);
+
+ // This always needs to be set since the accessibility state can
+ // potentially change without the input view being re-created.
+ AccessibleKeyboardViewProxy.setView(mKeyboardView);
+
+ return mCurrentInputView;
+ }
+
+ private void postSetInputView(final View newInputView) {
+ mInputMethodService.mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (newInputView != null) {
+ mInputMethodService.setInputView(newInputView);
+ }
+ mInputMethodService.updateInputViewShown();
+ }
+ });
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (PREF_KEYBOARD_LAYOUT.equals(key)) {
+ final int layoutId = getKeyboardThemeIndex(mInputMethodService, sharedPreferences);
+ postSetInputView(createInputView(layoutId, false));
+ } else if (Settings.PREF_SETTINGS_KEY.equals(key)) {
+ mSettingsKeyEnabledInSettings = getSettingsKeyMode(sharedPreferences,
+ mInputMethodService);
+ postSetInputView(createInputView(mThemeIndex, true));
+ }
+ }
+
+ public void onAutoCorrectionStateChanged(boolean isAutoCorrection) {
+ if (mIsAutoCorrectionActive != isAutoCorrection) {
+ mIsAutoCorrectionActive = isAutoCorrection;
+ final LatinKeyboard keyboard = getLatinKeyboard();
+ if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) {
+ final Key invalidatedKey = keyboard.onAutoCorrectionStateChanged(isAutoCorrection);
+ final LatinKeyboardView keyboardView = getKeyboardView();
+ if (keyboardView != null)
+ keyboardView.invalidateKey(invalidatedKey);
+ }
+ }
+ }
+
+ private static boolean getSettingsKeyMode(SharedPreferences prefs, Context context) {
+ final Resources res = context.getResources();
+ final boolean showSettingsKeyOption = res.getBoolean(
+ R.bool.config_enable_show_settings_key_option);
+ if (showSettingsKeyOption) {
+ final String settingsKeyMode = prefs.getString(Settings.PREF_SETTINGS_KEY,
+ res.getString(DEFAULT_SETTINGS_KEY_MODE));
+ // We show the settings key when 1) SETTINGS_KEY_MODE_ALWAYS_SHOW or
+ // 2) SETTINGS_KEY_MODE_AUTO and there are two or more enabled IMEs on the system
+ if (settingsKeyMode.equals(res.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW))
+ || (settingsKeyMode.equals(res.getString(SETTINGS_KEY_MODE_AUTO))
+ && Utils.hasMultipleEnabledIMEsOrSubtypes(
+ (InputMethodManagerCompatWrapper.getInstance(context))))) {
+ return true;
+ }
+ return false;
+ }
+ // If the show settings key option is disabled, we always try showing the settings key.
+ return true;
+ }
+
+ private static int getF2KeyMode(SharedPreferences prefs, Context context,
+ EditorInfo attribute) {
+ final boolean clobberSettingsKey = Utils.inPrivateImeOptions(
+ context.getPackageName(), LatinIME.IME_OPTION_NO_SETTINGS_KEY, attribute);
+ final Resources res = context.getResources();
+ final String settingsKeyMode = prefs.getString(Settings.PREF_SETTINGS_KEY,
+ res.getString(DEFAULT_SETTINGS_KEY_MODE));
+ if (settingsKeyMode.equals(res.getString(SETTINGS_KEY_MODE_AUTO))) {
+ return clobberSettingsKey ? KeyboardId.F2KEY_MODE_SHORTCUT_IME
+ : KeyboardId.F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS;
+ } else if (settingsKeyMode.equals(res.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW))) {
+ return clobberSettingsKey ? KeyboardId.F2KEY_MODE_NONE : KeyboardId.F2KEY_MODE_SETTINGS;
+ } else { // SETTINGS_KEY_MODE_ALWAYS_HIDE
+ return KeyboardId.F2KEY_MODE_SHORTCUT_IME;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
new file mode 100644
index 000000000..9dc019c61
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -0,0 +1,1364 @@
+/*
+ * 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.keyboard;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region.Op;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.android.inputmethod.accessibility.AccessibilityUtils;
+import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
+import com.android.inputmethod.compat.FrameLayoutCompatUtils;
+import com.android.inputmethod.keyboard.internal.MiniKeyboardBuilder;
+import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
+import com.android.inputmethod.keyboard.internal.SwipeTracker;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.WeakHashMap;
+
+/**
+ * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and detecting key
+ * presses and touch movements.
+ *
+ * @attr ref R.styleable#KeyboardView_backgroundDimAmount
+ * @attr ref R.styleable#KeyboardView_keyBackground
+ * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
+ * @attr ref R.styleable#KeyboardView_keyLetterRatio
+ * @attr ref R.styleable#KeyboardView_keyLabelRatio
+ * @attr ref R.styleable#KeyboardView_keyHintLetterRatio
+ * @attr ref R.styleable#KeyboardView_keyUppercaseLetterRatio
+ * @attr ref R.styleable#KeyboardView_keyTextStyle
+ * @attr ref R.styleable#KeyboardView_keyPreviewLayout
+ * @attr ref R.styleable#KeyboardView_keyPreviewOffset
+ * @attr ref R.styleable#KeyboardView_keyPreviewHeight
+ * @attr ref R.styleable#KeyboardView_keyTextColor
+ * @attr ref R.styleable#KeyboardView_keyTextColorDisabled
+ * @attr ref R.styleable#KeyboardView_keyHintLetterColor
+ * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor
+ * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor
+ * @attr ref R.styleable#KeyboardView_verticalCorrection
+ * @attr ref R.styleable#KeyboardView_popupLayout
+ * @attr ref R.styleable#KeyboardView_shadowColor
+ * @attr ref R.styleable#KeyboardView_shadowRadius
+ */
+public class KeyboardView extends View implements PointerTracker.UIProxy {
+ private static final String TAG = KeyboardView.class.getSimpleName();
+ private static final boolean DEBUG_SHOW_ALIGN = false;
+ private static final boolean DEBUG_KEYBOARD_GRID = false;
+
+ private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = true;
+ private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true;
+
+ // Timing constants
+ private final int mKeyRepeatInterval;
+
+ // Miscellaneous constants
+ private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
+ private static final int HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL = -1;
+
+ // XML attribute
+ private final float mKeyLetterRatio;
+ private final int mKeyTextColor;
+ private final int mKeyTextInactivatedColor;
+ private final Typeface mKeyTextStyle;
+ private final float mKeyLabelRatio;
+ private final float mKeyHintLetterRatio;
+ private final float mKeyUppercaseLetterRatio;
+ private final int mShadowColor;
+ private final float mShadowRadius;
+ private final Drawable mKeyBackground;
+ private final float mBackgroundDimAmount;
+ private final float mKeyHysteresisDistance;
+ private final float mVerticalCorrection;
+ private final int mPreviewOffset;
+ private final int mPreviewHeight;
+ private final int mPopupLayout;
+ private final Drawable mKeyPopupHintIcon;
+ private final int mKeyHintLetterColor;
+ private final int mKeyUppercaseLetterInactivatedColor;
+ private final int mKeyUppercaseLetterActivatedColor;
+
+ // Main keyboard
+ private Keyboard mKeyboard;
+ private int mKeyLetterSize;
+ private int mKeyLabelSize;
+ private int mKeyHintLetterSize;
+ private int mKeyUppercaseLetterSize;
+
+ // Key preview
+ private boolean mInForeground;
+ private TextView mPreviewText;
+ private Drawable mPreviewBackground;
+ private float mPreviewTextRatio;
+ private int mPreviewTextSize;
+ private boolean mShowKeyPreviewPopup = true;
+ private final int mDelayBeforePreview;
+ private int mDelayAfterPreview;
+ private ViewGroup mPreviewPlacer;
+ private final int[] mCoordinates = new int[2];
+
+ // Mini keyboard
+ private PopupWindow mPopupWindow;
+ private PopupPanel mPopupMiniKeyboardPanel;
+ private final WeakHashMap<Key, PopupPanel> mPopupPanelCache =
+ new WeakHashMap<Key, PopupPanel>();
+
+ /** Listener for {@link KeyboardActionListener}. */
+ private KeyboardActionListener mKeyboardActionListener;
+
+ private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
+
+ // TODO: Let the PointerTracker class manage this pointer queue
+ private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue();
+
+ private final boolean mHasDistinctMultitouch;
+ private int mOldPointerCount = 1;
+ private int mOldKeyIndex;
+
+ protected KeyDetector mKeyDetector = new KeyDetector();
+
+ // Swipe gesture detector
+ protected GestureDetector mGestureDetector;
+ private final SwipeTracker mSwipeTracker = new SwipeTracker();
+ private final int mSwipeThreshold;
+ private final boolean mDisambiguateSwipe;
+
+ // Drawing
+ /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
+ private boolean mDrawPending;
+ /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
+ private boolean mKeyboardChanged;
+ /** The dirty region in the keyboard bitmap */
+ private final Rect mDirtyRect = new Rect();
+ /** The key to invalidate. */
+ private Key mInvalidatedKey;
+ /** The dirty region for single key drawing */
+ private final Rect mInvalidatedKeyRect = new Rect();
+ /** The keyboard bitmap for faster updates */
+ private Bitmap mBuffer;
+ /** The canvas for the above mutable keyboard bitmap */
+ private Canvas mCanvas;
+ private final Paint mPaint = new Paint();
+ private final Rect mPadding = new Rect();
+ private final Rect mTextBounds = new Rect();
+ // This map caches key label text height in pixel as value and key label text size as map key.
+ private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
+ // This map caches key label text width in pixel as value and key label text size as map key.
+ private final HashMap<Integer, Integer> mTextWidthCache = new HashMap<Integer, Integer>();
+ // Distance from horizontal center of the key, proportional to key label text height and width.
+ private static final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.45f;
+ private static final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f;
+ private static final String KEY_LABEL_REFERENCE_CHAR = "M";
+ private final int mKeyLabelHorizontalPadding;
+
+ private final UIHandler mHandler = new UIHandler();
+
+ class UIHandler extends Handler {
+ private static final int MSG_SHOW_KEY_PREVIEW = 1;
+ private static final int MSG_DISMISS_KEY_PREVIEW = 2;
+ private static final int MSG_REPEAT_KEY = 3;
+ private static final int MSG_LONGPRESS_KEY = 4;
+ private static final int MSG_LONGPRESS_SHIFT_KEY = 5;
+ private static final int MSG_IGNORE_DOUBLE_TAP = 6;
+
+ private boolean mInKeyRepeat;
+
+ @Override
+ public void handleMessage(Message msg) {
+ final PointerTracker tracker = (PointerTracker) msg.obj;
+ switch (msg.what) {
+ case MSG_SHOW_KEY_PREVIEW:
+ showKey(msg.arg1, tracker);
+ break;
+ case MSG_DISMISS_KEY_PREVIEW:
+ mPreviewText.setVisibility(View.INVISIBLE);
+ break;
+ case MSG_REPEAT_KEY:
+ tracker.onRepeatKey(msg.arg1);
+ startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker);
+ break;
+ case MSG_LONGPRESS_KEY:
+ openMiniKeyboardIfRequired(msg.arg1, tracker);
+ break;
+ case MSG_LONGPRESS_SHIFT_KEY:
+ onLongPressShiftKey(tracker);
+ break;
+ }
+ }
+
+ public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) {
+ removeMessages(MSG_SHOW_KEY_PREVIEW);
+ if (mPreviewText.getVisibility() == VISIBLE || delay == 0) {
+ // Show right away, if it's already visible and finger is moving around
+ showKey(keyIndex, tracker);
+ } else {
+ sendMessageDelayed(
+ obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay);
+ }
+ }
+
+ public void cancelShowKeyPreview(PointerTracker tracker) {
+ removeMessages(MSG_SHOW_KEY_PREVIEW, tracker);
+ }
+
+ public void cancelAllShowKeyPreviews() {
+ removeMessages(MSG_SHOW_KEY_PREVIEW);
+ }
+
+ public void dismissKeyPreview(long delay, PointerTracker tracker) {
+ sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
+ }
+
+ public void cancelDismissKeyPreview(PointerTracker tracker) {
+ removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
+ }
+
+ public void cancelAllDismissKeyPreviews() {
+ removeMessages(MSG_DISMISS_KEY_PREVIEW);
+ }
+
+ public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
+ mInKeyRepeat = true;
+ sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
+ }
+
+ public void cancelKeyRepeatTimer() {
+ mInKeyRepeat = false;
+ removeMessages(MSG_REPEAT_KEY);
+ }
+
+ public boolean isInKeyRepeat() {
+ return mInKeyRepeat;
+ }
+
+ public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
+ cancelLongPressTimers();
+ sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
+ }
+
+ public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) {
+ cancelLongPressTimers();
+ if (ENABLE_CAPSLOCK_BY_LONGPRESS) {
+ sendMessageDelayed(
+ obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay);
+ }
+ }
+
+ public void cancelLongPressTimers() {
+ removeMessages(MSG_LONGPRESS_KEY);
+ removeMessages(MSG_LONGPRESS_SHIFT_KEY);
+ }
+
+ public void cancelKeyTimers() {
+ cancelKeyRepeatTimer();
+ cancelLongPressTimers();
+ removeMessages(MSG_IGNORE_DOUBLE_TAP);
+ }
+
+ public void startIgnoringDoubleTap() {
+ sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP),
+ ViewConfiguration.getDoubleTapTimeout());
+ }
+
+ public boolean isIgnoringDoubleTap() {
+ return hasMessages(MSG_IGNORE_DOUBLE_TAP);
+ }
+
+ public void cancelAllMessages() {
+ cancelKeyTimers();
+ cancelAllShowKeyPreviews();
+ cancelAllDismissKeyPreviews();
+ }
+ }
+
+ public KeyboardView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.keyboardViewStyle);
+ }
+
+ public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
+
+ mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground);
+ mKeyHysteresisDistance = a.getDimensionPixelOffset(
+ R.styleable.KeyboardView_keyHysteresisDistance, 0);
+ mVerticalCorrection = a.getDimensionPixelOffset(
+ R.styleable.KeyboardView_verticalCorrection, 0);
+ final int previewLayout = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
+ mPreviewOffset = a.getDimensionPixelOffset(R.styleable.KeyboardView_keyPreviewOffset, 0);
+ mPreviewHeight = a.getDimensionPixelSize(R.styleable.KeyboardView_keyPreviewHeight, 80);
+ mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio);
+ mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio);
+ mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
+ mKeyUppercaseLetterRatio = getRatio(a,
+ R.styleable.KeyboardView_keyUppercaseLetterRatio);
+ mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000);
+ mKeyTextInactivatedColor = a.getColor(
+ R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000);
+ mKeyPopupHintIcon = a.getDrawable(R.styleable.KeyboardView_keyPopupHintIcon);
+ mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0);
+ mKeyUppercaseLetterInactivatedColor = a.getColor(
+ R.styleable.KeyboardView_keyUppercaseLetterInactivatedColor, 0);
+ mKeyUppercaseLetterActivatedColor = a.getColor(
+ R.styleable.KeyboardView_keyUppercaseLetterActivatedColor, 0);
+ mKeyTextStyle = Typeface.defaultFromStyle(
+ a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL));
+ mPopupLayout = a.getResourceId(R.styleable.KeyboardView_popupLayout, 0);
+ mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0);
+ mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f);
+ // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount)
+ mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f);
+ a.recycle();
+
+ final Resources res = getResources();
+
+ if (previewLayout != 0) {
+ mPreviewText = (TextView) LayoutInflater.from(context).inflate(previewLayout, null);
+ mPreviewBackground = mPreviewText.getBackground();
+ mPreviewTextRatio = getRatio(res, R.fraction.key_preview_text_ratio);
+ } else {
+ mShowKeyPreviewPopup = false;
+ }
+ mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
+ mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
+ mKeyLabelHorizontalPadding = (int)res.getDimension(
+ R.dimen.key_label_horizontal_alignment_padding);
+
+ mPaint.setAntiAlias(true);
+ mPaint.setTextAlign(Align.CENTER);
+ mPaint.setAlpha(255);
+
+ mKeyBackground.getPadding(mPadding);
+
+ mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
+ // TODO: Refer to frameworks/base/core/res/res/values/config.xml
+ mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
+
+ GestureDetector.SimpleOnGestureListener listener =
+ new GestureDetector.SimpleOnGestureListener() {
+ private boolean mProcessingShiftDoubleTapEvent = false;
+
+ @Override
+ public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
+ float velocityY) {
+ final float absX = Math.abs(velocityX);
+ final float absY = Math.abs(velocityY);
+ float deltaY = me2.getY() - me1.getY();
+ int travelY = getHeight() / 2; // Half the keyboard height
+ mSwipeTracker.computeCurrentVelocity(1000);
+ final float endingVelocityY = mSwipeTracker.getYVelocity();
+ if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
+ if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
+ onSwipeDown();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent firstDown) {
+ if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard
+ && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) {
+ final int pointerIndex = firstDown.getActionIndex();
+ final int id = firstDown.getPointerId(pointerIndex);
+ final PointerTracker tracker = getPointerTracker(id);
+ // If the first down event is on shift key.
+ if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) {
+ mProcessingShiftDoubleTapEvent = true;
+ return true;
+ }
+ }
+ mProcessingShiftDoubleTapEvent = false;
+ return false;
+ }
+
+ @Override
+ public boolean onDoubleTapEvent(MotionEvent secondTap) {
+ if (mProcessingShiftDoubleTapEvent
+ && secondTap.getAction() == MotionEvent.ACTION_DOWN) {
+ final MotionEvent secondDown = secondTap;
+ final int pointerIndex = secondDown.getActionIndex();
+ final int id = secondDown.getPointerId(pointerIndex);
+ final PointerTracker tracker = getPointerTracker(id);
+ // If the second down event is also on shift key.
+ if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) {
+ // Detected a double tap on shift key. If we are in the ignoring double tap
+ // mode, it means we have already turned off caps lock in
+ // {@link KeyboardSwitcher#onReleaseShift} .
+ final boolean ignoringDoubleTap = mHandler.isIgnoringDoubleTap();
+ if (!ignoringDoubleTap)
+ onDoubleTapShiftKey(tracker);
+ return true;
+ }
+ // Otherwise these events should not be handled as double tap.
+ mProcessingShiftDoubleTapEvent = false;
+ }
+ return mProcessingShiftDoubleTapEvent;
+ }
+ };
+
+ final boolean ignoreMultitouch = true;
+ mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
+ mGestureDetector.setIsLongpressEnabled(false);
+
+ mHasDistinctMultitouch = context.getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
+ mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
+ }
+
+ // Read fraction value in TypedArray as float.
+ private static float getRatio(TypedArray a, int index) {
+ return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
+ }
+
+ // Read fraction value in resource as float.
+ private static float getRatio(Resources res, int id) {
+ return res.getFraction(id, 1000, 1000) / 1000.0f;
+ }
+
+ public void startIgnoringDoubleTap() {
+ if (ENABLE_CAPSLOCK_BY_DOUBLETAP)
+ mHandler.startIgnoringDoubleTap();
+ }
+
+ public void setOnKeyboardActionListener(KeyboardActionListener listener) {
+ mKeyboardActionListener = listener;
+ for (PointerTracker tracker : mPointerTrackers) {
+ tracker.setOnKeyboardActionListener(listener);
+ }
+ }
+
+ /**
+ * Returns the {@link KeyboardActionListener} object.
+ * @return the listener attached to this keyboard
+ */
+ protected KeyboardActionListener getOnKeyboardActionListener() {
+ return mKeyboardActionListener;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ // TODO: Should notify InputMethodService instead?
+ KeyboardSwitcher.getInstance().onSizeChanged();
+ }
+
+ /**
+ * Attaches a keyboard to this view. The keyboard can be switched at any time and the
+ * view will re-layout itself to accommodate the keyboard.
+ * @see Keyboard
+ * @see #getKeyboard()
+ * @param keyboard the keyboard to display in this view
+ */
+ public void setKeyboard(Keyboard keyboard) {
+ if (mKeyboard != null) {
+ dismissAllKeyPreviews();
+ }
+ // Remove any pending messages, except dismissing preview
+ mHandler.cancelKeyTimers();
+ mHandler.cancelAllShowKeyPreviews();
+ mKeyboard = keyboard;
+ LatinImeLogger.onSetKeyboard(keyboard);
+ mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
+ -getPaddingTop() + mVerticalCorrection);
+ for (PointerTracker tracker : mPointerTrackers) {
+ tracker.setKeyboard(keyboard, mKeyHysteresisDistance);
+ }
+ requestLayout();
+ mKeyboardChanged = true;
+ invalidateAllKeys();
+ mKeyDetector.setProximityThreshold(keyboard.getMostCommonKeyWidth());
+ mPopupPanelCache.clear();
+ final int keyHeight = keyboard.getRowHeight() - keyboard.getVerticalGap();
+ mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
+ mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
+ mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio);
+ mKeyUppercaseLetterSize = (int)(
+ keyHeight * mKeyUppercaseLetterRatio);
+ mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio);
+ }
+
+ /**
+ * Returns the current keyboard being displayed by this view.
+ * @return the currently attached keyboard
+ * @see #setKeyboard(Keyboard)
+ */
+ public Keyboard getKeyboard() {
+ return mKeyboard;
+ }
+
+ /**
+ * Returns whether the device has distinct multi-touch panel.
+ * @return true if the device has distinct multi-touch panel.
+ */
+ @Override
+ public boolean hasDistinctMultitouch() {
+ return mHasDistinctMultitouch;
+ }
+
+ /**
+ * Enables or disables the key feedback popup. This is a popup that shows a magnified
+ * version of the depressed key. By default the preview is enabled.
+ * @param previewEnabled whether or not to enable the key feedback preview
+ * @param delay the delay after which the preview is dismissed
+ * @see #isKeyPreviewPopupEnabled()
+ */
+ public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
+ mShowKeyPreviewPopup = previewEnabled;
+ mDelayAfterPreview = delay;
+ }
+
+ /**
+ * Returns the enabled state of the key feedback preview
+ * @return whether or not the key feedback preview is enabled
+ * @see #setKeyPreviewPopupEnabled(boolean, int)
+ */
+ public boolean isKeyPreviewPopupEnabled() {
+ return mShowKeyPreviewPopup;
+ }
+
+ /**
+ * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
+ * codes for adjacent keys. When disabled, only the primary key code will be
+ * reported.
+ * @param enabled whether or not the proximity correction is enabled
+ */
+ public void setProximityCorrectionEnabled(boolean enabled) {
+ mKeyDetector.setProximityCorrectionEnabled(enabled);
+ }
+
+ /**
+ * Returns true if proximity correction is enabled.
+ */
+ public boolean isProximityCorrectionEnabled() {
+ return mKeyDetector.isProximityCorrectionEnabled();
+ }
+
+ protected CharSequence adjustCase(CharSequence label) {
+ if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3
+ && Character.isLowerCase(label.charAt(0))) {
+ return label.toString().toUpperCase();
+ }
+ return label;
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Round up a little
+ if (mKeyboard == null) {
+ setMeasuredDimension(
+ getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
+ } else {
+ int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
+ if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
+ width = MeasureSpec.getSize(widthMeasureSpec);
+ }
+ setMeasuredDimension(
+ width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mDrawPending || mBuffer == null || mKeyboardChanged) {
+ onBufferDraw();
+ }
+ canvas.drawBitmap(mBuffer, 0, 0, null);
+ }
+
+ private void onBufferDraw() {
+ final int width = getWidth();
+ final int height = getHeight();
+ if (width == 0 || height == 0)
+ return;
+ if (mBuffer == null || mKeyboardChanged) {
+ mKeyboardChanged = false;
+ mDirtyRect.union(0, 0, width, height);
+ }
+ if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
+ if (mBuffer != null)
+ mBuffer.recycle();
+ mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ if (mCanvas != null) {
+ mCanvas.setBitmap(mBuffer);
+ } else {
+ mCanvas = new Canvas(mBuffer);
+ }
+ }
+ final Canvas canvas = mCanvas;
+ canvas.clipRect(mDirtyRect, Op.REPLACE);
+ canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
+
+ if (mKeyboard == null) return;
+
+ if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) {
+ // Draw a single key.
+ onBufferDrawKey(canvas, mInvalidatedKey);
+ } else {
+ // Draw all keys.
+ for (final Key key : mKeyboard.getKeys()) {
+ onBufferDrawKey(canvas, key);
+ }
+ }
+
+ // TODO: Move this function to ProximityInfo for getting rid of
+ // public declarations for
+ // GRID_WIDTH and GRID_HEIGHT
+ if (DEBUG_KEYBOARD_GRID) {
+ Paint p = new Paint();
+ p.setStyle(Paint.Style.STROKE);
+ p.setStrokeWidth(1.0f);
+ p.setColor(0x800000c0);
+ int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1)
+ / mKeyboard.GRID_WIDTH;
+ int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1)
+ / mKeyboard.GRID_HEIGHT;
+ for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++)
+ canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p);
+ for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++)
+ canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p);
+ }
+
+ // Overlay a dark rectangle to dim the keyboard
+ if (mPopupMiniKeyboardPanel != null) {
+ mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
+ canvas.drawRect(0, 0, width, height, mPaint);
+ }
+
+ mInvalidatedKey = null;
+ mDrawPending = false;
+ mDirtyRect.setEmpty();
+ }
+
+ private void onBufferDrawKey(final Canvas canvas, final Key key) {
+ final Paint paint = mPaint;
+ final Drawable keyBackground = mKeyBackground;
+ final Rect padding = mPadding;
+ final int kbdPaddingLeft = getPaddingLeft();
+ final int kbdPaddingTop = getPaddingTop();
+ final int keyDrawX = key.mX + key.mVisualInsetsLeft;
+ final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+ final int rowHeight = padding.top + key.mHeight;
+ final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase();
+
+ canvas.translate(keyDrawX + kbdPaddingLeft, key.mY + kbdPaddingTop);
+
+ // Draw key background.
+ final int[] drawableState = key.getCurrentDrawableState();
+ keyBackground.setState(drawableState);
+ final Rect bounds = keyBackground.getBounds();
+ if (keyDrawWidth != bounds.right || key.mHeight != bounds.bottom) {
+ keyBackground.setBounds(0, 0, keyDrawWidth, key.mHeight);
+ }
+ keyBackground.draw(canvas);
+
+ // Draw key label.
+ if (key.mLabel != null) {
+ // Switch the character to uppercase if shift is pressed
+ final String label = key.mLabel == null ? null : adjustCase(key.mLabel).toString();
+ // For characters, use large font. For labels like "Done", use small font.
+ final int labelSize = getLabelSizeAndSetPaint(label, key.mLabelOption, paint);
+ final int labelCharHeight = getLabelCharHeight(labelSize, paint);
+
+ // Vertical label text alignment.
+ final float baseline;
+ if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_BOTTOM) != 0) {
+ baseline = key.mHeight - labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR;
+ if (DEBUG_SHOW_ALIGN)
+ drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000,
+ new Paint());
+ } else { // Align center
+ final float centerY = (key.mHeight + padding.top - padding.bottom) / 2;
+ baseline = centerY + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER;
+ if (DEBUG_SHOW_ALIGN)
+ drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000,
+ new Paint());
+ }
+ // Horizontal label text alignment
+ final int positionX;
+ if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT) != 0) {
+ positionX = mKeyLabelHorizontalPadding + padding.left;
+ paint.setTextAlign(Align.LEFT);
+ if (DEBUG_SHOW_ALIGN)
+ drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint());
+ } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_RIGHT) != 0) {
+ positionX = keyDrawWidth - mKeyLabelHorizontalPadding - padding.right;
+ paint.setTextAlign(Align.RIGHT);
+ if (DEBUG_SHOW_ALIGN)
+ drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint());
+ } else {
+ positionX = (keyDrawWidth + padding.left - padding.right) / 2;
+ paint.setTextAlign(Align.CENTER);
+ if (DEBUG_SHOW_ALIGN) {
+ if (label.length() > 1)
+ drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint());
+ }
+ }
+ if (key.hasUppercaseLetter() && isManualTemporaryUpperCase) {
+ paint.setColor(mKeyTextInactivatedColor);
+ } else {
+ paint.setColor(mKeyTextColor);
+ }
+ if (key.isEnabled()) {
+ // Set a drop shadow for the text
+ paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
+ } else {
+ // Make label invisible
+ paint.setColor(Color.TRANSPARENT);
+ }
+ canvas.drawText(label, positionX, baseline, paint);
+ // Turn off drop shadow
+ paint.setShadowLayer(0, 0, 0, 0);
+ }
+
+ // Draw hint letter.
+ if (key.mHintLetter != null) {
+ final String label = key.mHintLetter.toString();
+ final int textColor;
+ final int textSize;
+ if (key.hasUppercaseLetter()) {
+ textColor = isManualTemporaryUpperCase ? mKeyUppercaseLetterActivatedColor
+ : mKeyUppercaseLetterInactivatedColor;
+ textSize = mKeyUppercaseLetterSize;
+ } else {
+ textColor = mKeyHintLetterColor;
+ textSize = mKeyHintLetterSize;
+ }
+ paint.setColor(textColor);
+ paint.setTextSize(textSize);
+ // Note: padding.right for drawX?
+ final float drawX = keyDrawWidth - getLabelCharWidth(textSize, paint);
+ final float drawY = -paint.ascent() + padding.top;
+ canvas.drawText(label, drawX, drawY, paint);
+ }
+
+ // Draw key icon.
+ final Drawable icon = key.getIcon();
+ if (key.mLabel == null && icon != null) {
+ final int drawableWidth = icon.getIntrinsicWidth();
+ final int drawableHeight = icon.getIntrinsicHeight();
+ final int drawableX;
+ final int drawableY = (key.mHeight + padding.top - padding.bottom - drawableHeight) / 2;
+ if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT) != 0) {
+ drawableX = padding.left + mKeyLabelHorizontalPadding;
+ if (DEBUG_SHOW_ALIGN)
+ drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint());
+ } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_RIGHT) != 0) {
+ drawableX = keyDrawWidth - padding.right - mKeyLabelHorizontalPadding
+ - drawableWidth;
+ if (DEBUG_SHOW_ALIGN)
+ drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight,
+ 0xc0808000, new Paint());
+ } else { // Align center
+ drawableX = (keyDrawWidth + padding.left - padding.right - drawableWidth) / 2;
+ if (DEBUG_SHOW_ALIGN)
+ drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight,
+ 0xc0008080, new Paint());
+ }
+ drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight);
+ if (DEBUG_SHOW_ALIGN)
+ drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
+ 0x80c00000, new Paint());
+ }
+
+ // Draw popup hint icon "...".
+ // TODO: Draw "..." by text.
+ if (key.hasPopupHint()) {
+ final int drawableWidth = keyDrawWidth;
+ final int drawableHeight = key.mHeight;
+ final int drawableX = 0;
+ final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL;
+ final Drawable hintIcon = mKeyPopupHintIcon;
+ drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight);
+ if (DEBUG_SHOW_ALIGN)
+ drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
+ 0x80c0c000, new Paint());
+ }
+
+ canvas.translate(-keyDrawX - kbdPaddingLeft, -key.mY - kbdPaddingTop);
+ }
+
+ public int getLabelSizeAndSetPaint(CharSequence label, int keyLabelOption, Paint paint) {
+ // For characters, use large font. For labels like "Done", use small font.
+ final int labelSize;
+ final Typeface labelStyle;
+ if ((keyLabelOption & Key.LABEL_OPTION_FONT_NORMAL) != 0) {
+ labelStyle = Typeface.DEFAULT;
+ } else if ((keyLabelOption & Key.LABEL_OPTION_FONT_FIXED_WIDTH) != 0) {
+ labelStyle = Typeface.MONOSPACE;
+ } else {
+ labelStyle = mKeyTextStyle;
+ }
+ if (label.length() > 1) {
+ labelSize = (keyLabelOption & Key.LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO) != 0
+ ? mKeyLetterSize : mKeyLabelSize;
+ } else {
+ labelSize = mKeyLetterSize;
+ }
+ paint.setTextSize(labelSize);
+ paint.setTypeface(labelStyle);
+ return labelSize;
+ }
+
+ private int getLabelCharHeight(int labelSize, Paint paint) {
+ Integer labelHeightValue = mTextHeightCache.get(labelSize);
+ final int labelCharHeight;
+ if (labelHeightValue != null) {
+ labelCharHeight = labelHeightValue;
+ } else {
+ paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, mTextBounds);
+ labelCharHeight = mTextBounds.height();
+ mTextHeightCache.put(labelSize, labelCharHeight);
+ }
+ return labelCharHeight;
+ }
+
+ private int getLabelCharWidth(int labelSize, Paint paint) {
+ Integer labelWidthValue = mTextWidthCache.get(labelSize);
+ final int labelCharWidth;
+ if (labelWidthValue != null) {
+ labelCharWidth = labelWidthValue;
+ } else {
+ paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, mTextBounds);
+ labelCharWidth = mTextBounds.width();
+ mTextWidthCache.put(labelSize, labelCharWidth);
+ }
+ return labelCharWidth;
+ }
+
+ private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
+ int height) {
+ canvas.translate(x, y);
+ icon.setBounds(0, 0, width, height);
+ icon.draw(canvas);
+ canvas.translate(-x, -y);
+ }
+
+ private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(1.0f);
+ paint.setColor(color);
+ canvas.drawLine(0, y, w, y, paint);
+ }
+
+ private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(1.0f);
+ paint.setColor(color);
+ canvas.drawLine(x, 0, x, h, paint);
+ }
+
+ private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color,
+ Paint paint) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(1.0f);
+ paint.setColor(color);
+ canvas.translate(x, y);
+ canvas.drawRect(0, 0, w, h, paint);
+ canvas.translate(-x, -y);
+ }
+
+ public void setForeground(boolean foreground) {
+ mInForeground = foreground;
+ }
+
+ // TODO: clean up this method.
+ private void dismissAllKeyPreviews() {
+ for (PointerTracker tracker : mPointerTrackers) {
+ tracker.setReleasedKeyGraphics();
+ dismissKeyPreview(tracker);
+ }
+ }
+
+ @Override
+ public void showKeyPreview(int keyIndex, PointerTracker tracker) {
+ if (mShowKeyPreviewPopup) {
+ mHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker);
+ } else if (mKeyboard.needSpacebarPreview(keyIndex)) {
+ // Show key preview (in this case, slide language switcher) without any delay.
+ showKey(keyIndex, tracker);
+ }
+ }
+
+ @Override
+ public void dismissKeyPreview(PointerTracker tracker) {
+ if (mShowKeyPreviewPopup) {
+ mHandler.cancelShowKeyPreview(tracker);
+ mHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
+ } else if (mKeyboard.needSpacebarPreview(KeyDetector.NOT_A_KEY)) {
+ // Dismiss key preview (in this case, slide language switcher) without any delay.
+ mPreviewText.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ private void addKeyPreview(TextView keyPreview) {
+ if (mPreviewPlacer == null) {
+ mPreviewPlacer = FrameLayoutCompatUtils.getPlacer(
+ (ViewGroup)getRootView().findViewById(android.R.id.content));
+ }
+ final ViewGroup placer = mPreviewPlacer;
+ placer.addView(keyPreview, FrameLayoutCompatUtils.newLayoutParam(placer, 0, 0));
+ }
+
+ // TODO: Introduce minimum duration for displaying key previews
+ // TODO: Display up to two key previews when the user presses two keys at the same time
+ private void showKey(final int keyIndex, PointerTracker tracker) {
+ final TextView previewText = mPreviewText;
+ // If the key preview has no parent view yet, add it to the ViewGroup which can place
+ // key preview absolutely in SoftInputWindow.
+ if (previewText.getParent() == null) {
+ addKeyPreview(previewText);
+ }
+
+ final Key key = tracker.getKey(keyIndex);
+ // If keyIndex is invalid or IME is already closed, we must not show key preview.
+ // Trying to show key preview while root window is closed causes
+ // WindowManager.BadTokenException.
+ if (key == null || !mInForeground)
+ return;
+
+ mHandler.cancelAllDismissKeyPreviews();
+
+ final int keyDrawX = key.mX + key.mVisualInsetsLeft;
+ final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+ // What we show as preview should match what we show on key top in onBufferDraw().
+ if (key.mLabel != null) {
+ // TODO Should take care of temporaryShiftLabel here.
+ previewText.setCompoundDrawables(null, null, null, null);
+ if (key.mLabel.length() > 1) {
+ previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize);
+ previewText.setTypeface(Typeface.DEFAULT_BOLD);
+ } else {
+ previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSize);
+ previewText.setTypeface(mKeyTextStyle);
+ }
+ previewText.setText(adjustCase(tracker.getPreviewText(key)));
+ } else {
+ final Drawable previewIcon = key.getPreviewIcon();
+ previewText.setCompoundDrawables(null, null, null,
+ previewIcon != null ? previewIcon : key.getIcon());
+ previewText.setText(null);
+ }
+ if (key.mCode == Keyboard.CODE_SPACE) {
+ previewText.setBackgroundColor(Color.TRANSPARENT);
+ } else {
+ previewText.setBackgroundDrawable(mPreviewBackground);
+ }
+ // Set the preview background state
+ previewText.getBackground().setState(
+ key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
+
+ previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth
+ + previewText.getPaddingLeft() + previewText.getPaddingRight());
+ final int previewHeight = mPreviewHeight;
+ getLocationInWindow(mCoordinates);
+ final int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0];
+ final int previewY = key.mY - previewHeight + mCoordinates[1] + mPreviewOffset;
+
+ // Place the key preview.
+ // TODO: Adjust position of key previews which touch screen edges
+ FrameLayoutCompatUtils.placeViewAt(
+ previewText, previewX, previewY, previewWidth, previewHeight);
+ previewText.setVisibility(VISIBLE);
+ }
+
+ /**
+ * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
+ * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
+ * draws the cached buffer.
+ * @see #invalidateKey(Key)
+ */
+ public void invalidateAllKeys() {
+ mDirtyRect.union(0, 0, getWidth(), getHeight());
+ mDrawPending = true;
+ invalidate();
+ }
+
+ /**
+ * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
+ * one key is changing it's content. Any changes that affect the position or size of the key
+ * may not be honored.
+ * @param key key in the attached {@link Keyboard}.
+ * @see #invalidateAllKeys
+ */
+ @Override
+ public void invalidateKey(Key key) {
+ if (key == null)
+ return;
+ mInvalidatedKey = key;
+ final int x = key.mX + getPaddingLeft();
+ final int y = key.mY + getPaddingTop();
+ mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight);
+ mDirtyRect.union(mInvalidatedKeyRect);
+ onBufferDraw();
+ invalidate(mInvalidatedKeyRect);
+ }
+
+ private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) {
+ // Check if we have a popup layout specified first.
+ if (mPopupLayout == 0) {
+ return false;
+ }
+
+ final Key parentKey = tracker.getKey(keyIndex);
+ if (parentKey == null)
+ return false;
+ boolean result = onLongPress(parentKey, tracker);
+ if (result) {
+ dismissAllKeyPreviews();
+ tracker.onLongPressed(mPointerQueue);
+ }
+ return result;
+ }
+
+ private void onLongPressShiftKey(PointerTracker tracker) {
+ tracker.onLongPressed(mPointerQueue);
+ mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
+ }
+
+ private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) {
+ // When shift key is double tapped, the first tap is correctly processed as usual tap. And
+ // the second tap is treated as this double tap event, so that we need not mark tracker
+ // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue.
+ mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
+ }
+
+ // This default implementation returns a popup mini keyboard panel.
+ // A derived class may return a language switcher popup panel, for instance.
+ protected PopupPanel onCreatePopupPanel(Key parentKey) {
+ if (parentKey.mPopupCharacters == null)
+ return null;
+
+ final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null);
+ if (container == null)
+ throw new NullPointerException();
+
+ final PopupMiniKeyboardView miniKeyboardView =
+ (PopupMiniKeyboardView)container.findViewById(R.id.mini_keyboard_view);
+ miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() {
+ @Override
+ public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
+ mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y);
+ dismissMiniKeyboard();
+ }
+
+ @Override
+ public void onTextInput(CharSequence text) {
+ mKeyboardActionListener.onTextInput(text);
+ dismissMiniKeyboard();
+ }
+
+ @Override
+ public void onCancelInput() {
+ mKeyboardActionListener.onCancelInput();
+ dismissMiniKeyboard();
+ }
+
+ @Override
+ public void onSwipeDown() {
+ // Nothing to do.
+ }
+ @Override
+ public void onPress(int primaryCode, boolean withSliding) {
+ mKeyboardActionListener.onPress(primaryCode, withSliding);
+ }
+ @Override
+ public void onRelease(int primaryCode, boolean withSliding) {
+ mKeyboardActionListener.onRelease(primaryCode, withSliding);
+ }
+ });
+
+ final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(),
+ parentKey, mKeyboard).build();
+ miniKeyboardView.setKeyboard(keyboard);
+
+ container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+
+ return miniKeyboardView;
+ }
+
+ /**
+ * Called when a key is long pressed. By default this will open mini keyboard associated
+ * with this key.
+ * @param parentKey the key that was long pressed
+ * @param tracker the pointer tracker which pressed the parent key
+ * @return true if the long press is handled, false otherwise. Subclasses should call the
+ * method on the base class if the subclass doesn't wish to handle the call.
+ */
+ protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
+ PopupPanel popupPanel = mPopupPanelCache.get(parentKey);
+ if (popupPanel == null) {
+ popupPanel = onCreatePopupPanel(parentKey);
+ if (popupPanel == null)
+ return false;
+ mPopupPanelCache.put(parentKey, popupPanel);
+ }
+ if (mPopupWindow == null) {
+ mPopupWindow = new PopupWindow(getContext());
+ mPopupWindow.setBackgroundDrawable(null);
+ mPopupWindow.setAnimationStyle(R.style.PopupMiniKeyboardAnimation);
+ // Allow popup window to be drawn off the screen.
+ mPopupWindow.setClippingEnabled(false);
+ }
+ mPopupMiniKeyboardPanel = popupPanel;
+ popupPanel.showPanel(this, parentKey, tracker, mPopupWindow);
+
+ invalidateAllKeys();
+ return true;
+ }
+
+ private PointerTracker getPointerTracker(final int id) {
+ final ArrayList<PointerTracker> pointers = mPointerTrackers;
+ final KeyboardActionListener listener = mKeyboardActionListener;
+
+ // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
+ for (int i = pointers.size(); i <= id; i++) {
+ final PointerTracker tracker =
+ new PointerTracker(i, this, mHandler, mKeyDetector, this);
+ if (mKeyboard != null)
+ tracker.setKeyboard(mKeyboard, mKeyHysteresisDistance);
+ if (listener != null)
+ tracker.setOnKeyboardActionListener(listener);
+ pointers.add(tracker);
+ }
+
+ return pointers.get(id);
+ }
+
+ public boolean isInSlidingKeyInput() {
+ if (mPopupMiniKeyboardPanel != null) {
+ return mPopupMiniKeyboardPanel.isInSlidingKeyInput();
+ } else {
+ return mPointerQueue.isInSlidingKeyInput();
+ }
+ }
+
+ public int getPointerCount() {
+ return mOldPointerCount;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ final int action = me.getActionMasked();
+ final int pointerCount = me.getPointerCount();
+ final int oldPointerCount = mOldPointerCount;
+ mOldPointerCount = pointerCount;
+
+ // TODO: cleanup this code into a multi-touch to single-touch event converter class?
+ // If the device does not have distinct multi-touch support panel, ignore all multi-touch
+ // events except a transition from/to single-touch.
+ if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
+ return true;
+ }
+
+ // Track the last few movements to look for spurious swipes.
+ mSwipeTracker.addMovement(me);
+
+ // Gesture detector must be enabled only when mini-keyboard is not on the screen.
+ if (mPopupMiniKeyboardPanel == null && mGestureDetector != null
+ && mGestureDetector.onTouchEvent(me)) {
+ dismissAllKeyPreviews();
+ mHandler.cancelKeyTimers();
+ return true;
+ }
+
+ final long eventTime = me.getEventTime();
+ final int index = me.getActionIndex();
+ final int id = me.getPointerId(index);
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
+
+ // Needs to be called after the gesture detector gets a turn, as it may have displayed the
+ // mini keyboard
+ if (mPopupMiniKeyboardPanel != null) {
+ return mPopupMiniKeyboardPanel.onTouchEvent(me);
+ }
+
+ if (mHandler.isInKeyRepeat()) {
+ final PointerTracker tracker = getPointerTracker(id);
+ // Key repeating timer will be canceled if 2 or more keys are in action, and current
+ // event (UP or DOWN) is non-modifier key.
+ if (pointerCount > 1 && !tracker.isModifier()) {
+ mHandler.cancelKeyRepeatTimer();
+ }
+ // Up event will pass through.
+ }
+
+ // TODO: cleanup this code into a multi-touch to single-touch event converter class?
+ // Translate mutli-touch event to single-touch events on the device that has no distinct
+ // multi-touch panel.
+ if (!mHasDistinctMultitouch) {
+ // Use only main (id=0) pointer tracker.
+ PointerTracker tracker = getPointerTracker(0);
+ if (pointerCount == 1 && oldPointerCount == 2) {
+ // Multi-touch to single touch transition.
+ // Send a down event for the latest pointer if the key is different from the
+ // previous key.
+ final int newKeyIndex = tracker.getKeyIndexOn(x, y);
+ if (mOldKeyIndex != newKeyIndex) {
+ tracker.onDownEvent(x, y, eventTime, null);
+ if (action == MotionEvent.ACTION_UP)
+ tracker.onUpEvent(x, y, eventTime, null);
+ }
+ } else if (pointerCount == 2 && oldPointerCount == 1) {
+ // Single-touch to multi-touch transition.
+ // Send an up event for the last pointer.
+ final int lastX = tracker.getLastX();
+ final int lastY = tracker.getLastY();
+ mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY);
+ tracker.onUpEvent(lastX, lastY, eventTime, null);
+ } else if (pointerCount == 1 && oldPointerCount == 1) {
+ tracker.onTouchEvent(action, x, y, eventTime, null);
+ } else {
+ Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
+ + " (old " + oldPointerCount + ")");
+ }
+ return true;
+ }
+
+ final PointerTrackerQueue queue = mPointerQueue;
+ if (action == MotionEvent.ACTION_MOVE) {
+ for (int i = 0; i < pointerCount; i++) {
+ final PointerTracker tracker = getPointerTracker(me.getPointerId(i));
+ tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue);
+ }
+ } else {
+ final PointerTracker tracker = getPointerTracker(id);
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ tracker.onDownEvent(x, y, eventTime, queue);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ tracker.onUpEvent(x, y, eventTime, queue);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ tracker.onCancelEvent(x, y, eventTime, queue);
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ protected void onSwipeDown() {
+ mKeyboardActionListener.onSwipeDown();
+ }
+
+ public void closing() {
+ mPreviewText.setVisibility(View.GONE);
+ mHandler.cancelAllMessages();
+
+ dismissMiniKeyboard();
+ mDirtyRect.union(0, 0, getWidth(), getHeight());
+ mPopupPanelCache.clear();
+ requestLayout();
+ }
+
+ public void purgeKeyboardAndClosing() {
+ mKeyboard = null;
+ closing();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ closing();
+ }
+
+ private boolean dismissMiniKeyboard() {
+ if (mPopupWindow != null && mPopupWindow.isShowing()) {
+ mPopupWindow.dismiss();
+ mPopupMiniKeyboardPanel = null;
+ invalidateAllKeys();
+ return true;
+ }
+ return false;
+ }
+
+ public boolean handleBack() {
+ return dismissMiniKeyboard();
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+ return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event)
+ || super.dispatchTouchEvent(event);
+ }
+
+ return super.dispatchTouchEvent(event);
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+ final PointerTracker tracker = getPointerTracker(0);
+ return AccessibleKeyboardViewProxy.getInstance().dispatchPopulateAccessibilityEvent(
+ event, tracker) || super.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ return super.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ public boolean onHoverEvent(MotionEvent event) {
+ // Since reflection doesn't support calling superclass methods, this
+ // method checks for the existence of onHoverEvent() in the View class
+ // before returning a value.
+ if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+ final PointerTracker tracker = getPointerTracker(0);
+ return AccessibleKeyboardViewProxy.getInstance().onHoverEvent(event, tracker);
+ }
+
+ return false;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboard.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboard.java
new file mode 100644
index 000000000..00bf348f2
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/LatinKeyboard.java
@@ -0,0 +1,425 @@
+/*
+ * 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.keyboard;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+import com.android.inputmethod.keyboard.internal.SlidingLocaleDrawable;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+
+import java.lang.ref.SoftReference;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+// TODO: We should remove this class
+public class LatinKeyboard extends Keyboard {
+ private static final int SPACE_LED_LENGTH_PERCENT = 80;
+
+ public static final int CODE_NEXT_LANGUAGE = -100;
+ public static final int CODE_PREV_LANGUAGE = -101;
+
+ private final Resources mRes;
+ private final Theme mTheme;
+ private final SubtypeSwitcher mSubtypeSwitcher = SubtypeSwitcher.getInstance();
+
+ /* Space key and its icons, drawables and colors. */
+ private final Key mSpaceKey;
+ private final Drawable mSpaceIcon;
+ private final Drawable mSpacePreviewIcon;
+ private final int mSpaceKeyIndex;
+ private final boolean mAutoCorrectionSpacebarLedEnabled;
+ private final Drawable mAutoCorrectionSpacebarLedIcon;
+ private final Drawable mSpacebarArrowLeftIcon;
+ private final Drawable mSpacebarArrowRightIcon;
+ private final int mSpacebarTextColor;
+ private final int mSpacebarTextShadowColor;
+ private float mSpacebarTextFadeFactor = 0.0f;
+ private final int mSpacebarLanguageSwitchThreshold;
+ private int mSpacebarSlidingLanguageSwitchDiff;
+ private final SlidingLocaleDrawable mSlidingLocaleIcon;
+ private final HashMap<Integer, SoftReference<BitmapDrawable>> mSpaceDrawableCache =
+ new HashMap<Integer, SoftReference<BitmapDrawable>>();
+
+ /* Shortcut key and its icons if available */
+ private final Key mShortcutKey;
+ private final Drawable mEnabledShortcutIcon;
+ private final Drawable mDisabledShortcutIcon;
+
+ // Minimum width of spacebar dragging to trigger the language switch (represented by the number
+ // of the most common key width of this keyboard).
+ private static final int SPACEBAR_DRAG_WIDTH = 3;
+ // Minimum width of space key preview (proportional to keyboard width).
+ private static final float SPACEBAR_POPUP_MIN_RATIO = 0.5f;
+ // Height in space key the language name will be drawn. (proportional to space key height)
+ public static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f;
+ // If the full language name needs to be smaller than this value to be drawn on space key,
+ // its short language name will be used instead.
+ private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f;
+
+ private static final String SMALL_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR = "small";
+ private static final String MEDIUM_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR = "medium";
+
+ public LatinKeyboard(Context context, KeyboardId id, int width) {
+ super(context, id.getXmlId(), id, width);
+ mRes = context.getResources();
+ mTheme = context.getTheme();
+
+ final List<Key> keys = getKeys();
+ int spaceKeyIndex = -1;
+ int shortcutKeyIndex = -1;
+ final int keyCount = keys.size();
+ for (int index = 0; index < keyCount; index++) {
+ // For now, assuming there are up to one space key and one shortcut key respectively.
+ switch (keys.get(index).mCode) {
+ case CODE_SPACE:
+ spaceKeyIndex = index;
+ break;
+ case CODE_SHORTCUT:
+ shortcutKeyIndex = index;
+ break;
+ }
+ }
+
+ // The index of space key is available only after Keyboard constructor has finished.
+ mSpaceKey = (spaceKeyIndex >= 0) ? keys.get(spaceKeyIndex) : null;
+ mSpaceIcon = (mSpaceKey != null) ? mSpaceKey.getIcon() : null;
+ mSpacePreviewIcon = (mSpaceKey != null) ? mSpaceKey.getPreviewIcon() : null;
+ mSpaceKeyIndex = spaceKeyIndex;
+
+ mShortcutKey = (shortcutKeyIndex >= 0) ? keys.get(shortcutKeyIndex) : null;
+ mEnabledShortcutIcon = (mShortcutKey != null) ? mShortcutKey.getIcon() : null;
+
+ final TypedArray a = context.obtainStyledAttributes(
+ null, R.styleable.LatinKeyboard, R.attr.latinKeyboardStyle, R.style.LatinKeyboard);
+ mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
+ R.styleable.LatinKeyboard_autoCorrectionSpacebarLedEnabled, false);
+ mAutoCorrectionSpacebarLedIcon = a.getDrawable(
+ R.styleable.LatinKeyboard_autoCorrectionSpacebarLedIcon);
+ mDisabledShortcutIcon = a.getDrawable(R.styleable.LatinKeyboard_disabledShortcutIcon);
+ mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboard_spacebarTextColor, 0);
+ mSpacebarTextShadowColor = a.getColor(
+ R.styleable.LatinKeyboard_spacebarTextShadowColor, 0);
+ mSpacebarArrowLeftIcon = a.getDrawable(
+ R.styleable.LatinKeyboard_spacebarArrowLeftIcon);
+ mSpacebarArrowRightIcon = a.getDrawable(
+ R.styleable.LatinKeyboard_spacebarArrowRightIcon);
+ a.recycle();
+
+ // The threshold is "key width" x 1.25
+ mSpacebarLanguageSwitchThreshold = (getMostCommonKeyWidth() * 5) / 4;
+
+ if (mSpaceKey != null && mSpacePreviewIcon != null) {
+ final int slidingIconWidth = Math.max(mSpaceKey.mWidth,
+ (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO));
+ final int spaceKeyheight = mSpacePreviewIcon.getIntrinsicHeight();
+ mSlidingLocaleIcon = new SlidingLocaleDrawable(
+ context, mSpacePreviewIcon, slidingIconWidth, spaceKeyheight);
+ mSlidingLocaleIcon.setBounds(0, 0, slidingIconWidth, spaceKeyheight);
+ } else {
+ mSlidingLocaleIcon = null;
+ }
+ }
+
+ public void setSpacebarTextFadeFactor(float fadeFactor, LatinKeyboardView view) {
+ mSpacebarTextFadeFactor = fadeFactor;
+ updateSpacebarForLocale(false);
+ if (view != null)
+ view.invalidateKey(mSpaceKey);
+ }
+
+ private static int getSpacebarTextColor(int color, float fadeFactor) {
+ final int newColor = Color.argb((int)(Color.alpha(color) * fadeFactor),
+ Color.red(color), Color.green(color), Color.blue(color));
+ return newColor;
+ }
+
+ private static ColorFilter getSpacebarDrawableFilter(float fadeFactor) {
+ final ColorMatrix colorMatrix = new ColorMatrix();
+ colorMatrix.setScale(1, 1, 1, fadeFactor);
+ return new ColorMatrixColorFilter(colorMatrix);
+ }
+
+ public void updateShortcutKey(boolean available, LatinKeyboardView view) {
+ if (mShortcutKey == null)
+ return;
+ mShortcutKey.setEnabled(available);
+ mShortcutKey.setIcon(available ? mEnabledShortcutIcon : mDisabledShortcutIcon);
+ if (view != null)
+ view.invalidateKey(mShortcutKey);
+ }
+
+ public boolean needsAutoCorrectionSpacebarLed() {
+ return mAutoCorrectionSpacebarLedEnabled;
+ }
+
+ /**
+ * @return a key which should be invalidated.
+ */
+ public Key onAutoCorrectionStateChanged(boolean isAutoCorrection) {
+ updateSpacebarForLocale(isAutoCorrection);
+ return mSpaceKey;
+ }
+
+ private void updateSpacebarForLocale(boolean isAutoCorrection) {
+ if (mSpaceKey == null)
+ return;
+ // If application locales are explicitly selected.
+ if (mSubtypeSwitcher.needsToDisplayLanguage()) {
+ mSpaceKey.setIcon(getSpaceDrawable(
+ mSubtypeSwitcher.getInputLocale(), isAutoCorrection));
+ } else if (isAutoCorrection) {
+ mSpaceKey.setIcon(getSpaceDrawable(null, true));
+ } else {
+ mSpaceKey.setIcon(mSpaceIcon);
+ }
+ }
+
+ // Compute width of text with specified text size using paint.
+ private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) {
+ paint.setTextSize(textSize);
+ paint.getTextBounds(text, 0, text.length(), bounds);
+ return bounds.width();
+ }
+
+ // Layout local language name and left and right arrow on spacebar.
+ private static String layoutSpacebar(Paint paint, Locale locale, Drawable icon, Drawable lArrow,
+ Drawable rArrow, int width, int height, float origTextSize) {
+ final float arrowWidth = lArrow.getIntrinsicWidth();
+ final float arrowHeight = lArrow.getIntrinsicHeight();
+ final float maxTextWidth = width - (arrowWidth + arrowWidth);
+ final Rect bounds = new Rect();
+
+ // Estimate appropriate language name text size to fit in maxTextWidth.
+ String language = SubtypeSwitcher.getFullDisplayName(locale, true);
+ int textWidth = getTextWidth(paint, language, origTextSize, bounds);
+ // Assuming text width and text size are proportional to each other.
+ float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
+ // allow variable text size
+ textWidth = getTextWidth(paint, language, textSize, bounds);
+ // If text size goes too small or text does not fit, use middle or short name
+ final boolean useMiddleName = (textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME)
+ || (textWidth > maxTextWidth);
+
+ final boolean useShortName;
+ if (useMiddleName) {
+ language = SubtypeSwitcher.getMiddleDisplayLanguage(locale);
+ textWidth = getTextWidth(paint, language, origTextSize, bounds);
+ textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
+ useShortName = (textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME)
+ || (textWidth > maxTextWidth);
+ } else {
+ useShortName = false;
+ }
+
+ if (useShortName) {
+ language = SubtypeSwitcher.getShortDisplayLanguage(locale);
+ textWidth = getTextWidth(paint, language, origTextSize, bounds);
+ textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
+ }
+ paint.setTextSize(textSize);
+
+ // Place left and right arrow just before and after language text.
+ final float textHeight = -paint.ascent() + paint.descent();
+ final float baseline = (icon != null) ? height * SPACEBAR_LANGUAGE_BASELINE
+ : height / 2 + textHeight / 2;
+ final int top = (int)(baseline - arrowHeight);
+ final float remains = (width - textWidth) / 2;
+ lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline);
+ rArrow.setBounds((int)(remains + textWidth), top, (int)(remains + textWidth + arrowWidth),
+ (int)baseline);
+
+ return language;
+ }
+
+ private BitmapDrawable getSpaceDrawable(Locale locale, boolean isAutoCorrection) {
+ final Integer hashCode = Arrays.hashCode(
+ new Object[] { locale, isAutoCorrection, mSpacebarTextFadeFactor });
+ final SoftReference<BitmapDrawable> ref = mSpaceDrawableCache.get(hashCode);
+ BitmapDrawable drawable = (ref == null) ? null : ref.get();
+ if (drawable == null) {
+ drawable = new BitmapDrawable(mRes, drawSpacebar(
+ locale, isAutoCorrection, mSpacebarTextFadeFactor));
+ mSpaceDrawableCache.put(hashCode, new SoftReference<BitmapDrawable>(drawable));
+ }
+ return drawable;
+ }
+
+ private Bitmap drawSpacebar(Locale inputLocale, boolean isAutoCorrection,
+ float textFadeFactor) {
+ final int width = mSpaceKey.mWidth;
+ final int height = mSpaceIcon != null ? mSpaceIcon.getIntrinsicHeight() : mSpaceKey.mHeight;
+ final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(buffer);
+ final Resources res = mRes;
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+
+ // If application locales are explicitly selected.
+ if (inputLocale != null) {
+ final Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setTextAlign(Align.CENTER);
+
+ final String textSizeOfLanguageOnSpacebar = res.getString(
+ R.string.config_text_size_of_language_on_spacebar,
+ SMALL_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR);
+ final int textStyle;
+ final int defaultTextSize;
+ if (MEDIUM_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR.equals(textSizeOfLanguageOnSpacebar)) {
+ textStyle = android.R.style.TextAppearance_Medium;
+ defaultTextSize = 18;
+ } else {
+ textStyle = android.R.style.TextAppearance_Small;
+ defaultTextSize = 14;
+ }
+
+ final String language = layoutSpacebar(paint, inputLocale, mSpaceIcon,
+ mSpacebarArrowLeftIcon, mSpacebarArrowRightIcon, width, height,
+ getTextSizeFromTheme(mTheme, textStyle, defaultTextSize));
+
+ // Draw language text with shadow
+ // In case there is no space icon, we will place the language text at the center of
+ // spacebar.
+ final float descent = paint.descent();
+ final float textHeight = -paint.ascent() + descent;
+ final float baseline = (mSpaceIcon != null) ? height * SPACEBAR_LANGUAGE_BASELINE
+ : height / 2 + textHeight / 2;
+ paint.setColor(getSpacebarTextColor(mSpacebarTextShadowColor, textFadeFactor));
+ canvas.drawText(language, width / 2, baseline - descent - 1, paint);
+ paint.setColor(getSpacebarTextColor(mSpacebarTextColor, textFadeFactor));
+ canvas.drawText(language, width / 2, baseline - descent, paint);
+
+ // Put arrows that are already laid out on either side of the text
+ // Because language switch is disabled on phone and number layouts, hide arrows.
+ // TODO: Sort out how to enable language switch on these layouts.
+ if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()
+ && mSubtypeSwitcher.getEnabledKeyboardLocaleCount() > 1
+ && !(isPhoneKeyboard() || isNumberKeyboard())) {
+ mSpacebarArrowLeftIcon.setColorFilter(getSpacebarDrawableFilter(textFadeFactor));
+ mSpacebarArrowRightIcon.setColorFilter(getSpacebarDrawableFilter(textFadeFactor));
+ mSpacebarArrowLeftIcon.draw(canvas);
+ mSpacebarArrowRightIcon.draw(canvas);
+ }
+ }
+
+ // Draw the spacebar icon at the bottom
+ if (isAutoCorrection) {
+ final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
+ final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
+ int x = (width - iconWidth) / 2;
+ int y = height - iconHeight;
+ mAutoCorrectionSpacebarLedIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
+ mAutoCorrectionSpacebarLedIcon.draw(canvas);
+ } else if (mSpaceIcon != null) {
+ final int iconWidth = mSpaceIcon.getIntrinsicWidth();
+ final int iconHeight = mSpaceIcon.getIntrinsicHeight();
+ int x = (width - iconWidth) / 2;
+ int y = height - iconHeight;
+ mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
+ mSpaceIcon.draw(canvas);
+ }
+ return buffer;
+ }
+
+ public void setSpacebarSlidingLanguageSwitchDiff(int diff) {
+ mSpacebarSlidingLanguageSwitchDiff = diff;
+ }
+
+ public void updateSpacebarPreviewIcon(int diff) {
+ if (mSpacebarSlidingLanguageSwitchDiff == diff)
+ return;
+ mSpacebarSlidingLanguageSwitchDiff = diff;
+ if (mSlidingLocaleIcon == null)
+ return;
+ mSlidingLocaleIcon.setDiff(diff);
+ if (Math.abs(diff) == Integer.MAX_VALUE) {
+ mSpaceKey.setPreviewIcon(mSpacePreviewIcon);
+ } else {
+ mSpaceKey.setPreviewIcon(mSlidingLocaleIcon);
+ }
+ mSpaceKey.getPreviewIcon().invalidateSelf();
+ }
+
+ public boolean shouldTriggerSpacebarSlidingLanguageSwitch(int diff) {
+ // On phone and number layouts, sliding language switch is disabled.
+ // TODO: Sort out how to enable language switch on these layouts.
+ if (isPhoneKeyboard() || isNumberKeyboard())
+ return false;
+ return Math.abs(diff) > mSpacebarLanguageSwitchThreshold;
+ }
+
+ /**
+ * Return true if spacebar needs showing preview even when "popup on keypress" is off.
+ * @param keyIndex index of the pressing key
+ * @return true if spacebar needs showing preview
+ */
+ @Override
+ public boolean needSpacebarPreview(int keyIndex) {
+ // This method is called when "popup on keypress" is off.
+ if (!mSubtypeSwitcher.useSpacebarLanguageSwitcher())
+ return false;
+ // Dismiss key preview.
+ if (keyIndex == KeyDetector.NOT_A_KEY)
+ return true;
+ // Key is not a spacebar.
+ if (keyIndex != mSpaceKeyIndex)
+ return false;
+ // The language switcher will be displayed only when the dragging distance is greater
+ // than the threshold.
+ return shouldTriggerSpacebarSlidingLanguageSwitch(mSpacebarSlidingLanguageSwitchDiff);
+ }
+
+ public int getLanguageChangeDirection() {
+ if (mSpaceKey == null || mSubtypeSwitcher.getEnabledKeyboardLocaleCount() <= 1
+ || Math.abs(mSpacebarSlidingLanguageSwitchDiff)
+ < getMostCommonKeyWidth() * SPACEBAR_DRAG_WIDTH) {
+ return 0; // No change
+ }
+ return mSpacebarSlidingLanguageSwitchDiff > 0 ? 1 : -1;
+ }
+
+ @Override
+ public int[] getNearestKeys(int x, int y) {
+ // Avoid dead pixels at edges of the keyboard
+ return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)),
+ Math.max(0, Math.min(y, getHeight() - 1)));
+ }
+
+ public static int getTextSizeFromTheme(Theme theme, int style, int defValue) {
+ TypedArray array = theme.obtainStyledAttributes(
+ style, new int[] { android.R.attr.textSize });
+ int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue);
+ return textSize;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
new file mode 100644
index 000000000..901df6ab7
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
@@ -0,0 +1,242 @@
+/*
+ * 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.keyboard;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.deprecated.VoiceProxy;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.Utils;
+
+// TODO: We should remove this class
+public class LatinKeyboardView extends KeyboardView {
+ private static final String TAG = LatinKeyboardView.class.getSimpleName();
+ private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
+
+ /** Whether we've started dropping move events because we found a big jump */
+ private boolean mDroppingEvents;
+ /**
+ * Whether multi-touch disambiguation needs to be disabled if a real multi-touch event has
+ * occured
+ */
+ private boolean mDisableDisambiguation;
+ /** The distance threshold at which we start treating the touch session as a multi-touch */
+ private int mJumpThresholdSquare = Integer.MAX_VALUE;
+ /** The y coordinate of the last row */
+ private int mLastRowY;
+ private int mLastX;
+ private int mLastY;
+
+ public LatinKeyboardView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
+ LatinKeyboard latinKeyboard = getLatinKeyboard();
+ if (latinKeyboard != null
+ && (latinKeyboard.isPhoneKeyboard() || latinKeyboard.isNumberKeyboard())) {
+ // Phone and number keyboard never shows popup preview (except language switch).
+ super.setKeyPreviewPopupEnabled(false, delay);
+ } else {
+ super.setKeyPreviewPopupEnabled(previewEnabled, delay);
+ }
+ }
+
+ @Override
+ public void setKeyboard(Keyboard newKeyboard) {
+ super.setKeyboard(newKeyboard);
+ // One-seventh of the keyboard width seems like a reasonable threshold
+ mJumpThresholdSquare = newKeyboard.getMinWidth() / 7;
+ mJumpThresholdSquare *= mJumpThresholdSquare;
+ // Assuming there are 4 rows, this is the coordinate of the last row
+ mLastRowY = (newKeyboard.getHeight() * 3) / 4;
+ }
+
+ private LatinKeyboard getLatinKeyboard() {
+ Keyboard keyboard = getKeyboard();
+ if (keyboard instanceof LatinKeyboard) {
+ return (LatinKeyboard)keyboard;
+ } else {
+ return null;
+ }
+ }
+
+ public void setSpacebarTextFadeFactor(float fadeFactor, LatinKeyboard oldKeyboard) {
+ final LatinKeyboard currentKeyboard = getLatinKeyboard();
+ // We should not set text fade factor to the keyboard which does not display the language on
+ // its spacebar.
+ if (currentKeyboard != null && currentKeyboard == oldKeyboard)
+ currentKeyboard.setSpacebarTextFadeFactor(fadeFactor, this);
+ }
+
+ @Override
+ protected boolean onLongPress(Key key, PointerTracker tracker) {
+ int primaryCode = key.mCode;
+ if (primaryCode == Keyboard.CODE_SETTINGS) {
+ return invokeOnKey(Keyboard.CODE_SETTINGS_LONGPRESS);
+ } else if (primaryCode == '0' && getLatinKeyboard().isPhoneKeyboard()) {
+ // Long pressing on 0 in phone number keypad gives you a '+'.
+ return invokeOnKey('+');
+ } else {
+ return super.onLongPress(key, tracker);
+ }
+ }
+
+ private boolean invokeOnKey(int primaryCode) {
+ getOnKeyboardActionListener().onCodeInput(primaryCode, null,
+ KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
+ KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+ return true;
+ }
+
+ @Override
+ protected CharSequence adjustCase(CharSequence label) {
+ LatinKeyboard keyboard = getLatinKeyboard();
+ if (keyboard.isAlphaKeyboard()
+ && keyboard.isShiftedOrShiftLocked()
+ && !TextUtils.isEmpty(label) && label.length() < 3
+ && Character.isLowerCase(label.charAt(0))) {
+ return label.toString().toUpperCase(keyboard.mId.mLocale);
+ }
+ return label;
+ }
+
+ /**
+ * This function checks to see if we need to handle any sudden jumps in the pointer location
+ * that could be due to a multi-touch being treated as a move by the firmware or hardware.
+ * Once a sudden jump is detected, all subsequent move events are discarded
+ * until an UP is received.<P>
+ * When a sudden jump is detected, an UP event is simulated at the last position and when
+ * the sudden moves subside, a DOWN event is simulated for the second key.
+ * @param me the motion event
+ * @return true if the event was consumed, so that it doesn't continue to be handled by
+ * KeyboardView.
+ */
+ private boolean handleSuddenJump(MotionEvent me) {
+ // If device has distinct multi touch panel, there is no need to check sudden jump.
+ if (hasDistinctMultitouch())
+ return false;
+ final int action = me.getAction();
+ final int x = (int) me.getX();
+ final int y = (int) me.getY();
+ boolean result = false;
+
+ // Real multi-touch event? Stop looking for sudden jumps
+ if (me.getPointerCount() > 1) {
+ mDisableDisambiguation = true;
+ }
+ if (mDisableDisambiguation) {
+ // If UP, reset the multi-touch flag
+ if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false;
+ return false;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ // Reset the "session"
+ mDroppingEvents = false;
+ mDisableDisambiguation = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // Is this a big jump?
+ final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y);
+ // Check the distance and also if the move is not entirely within the bottom row
+ // If it's only in the bottom row, it might be an intentional slide gesture
+ // for language switching
+ if (distanceSquare > mJumpThresholdSquare
+ && (mLastY < mLastRowY || y < mLastRowY)) {
+ // If we're not yet dropping events, start dropping and send an UP event
+ if (!mDroppingEvents) {
+ mDroppingEvents = true;
+ // Send an up event
+ MotionEvent translated = MotionEvent.obtain(
+ me.getEventTime(), me.getEventTime(),
+ MotionEvent.ACTION_UP,
+ mLastX, mLastY, me.getMetaState());
+ super.onTouchEvent(translated);
+ translated.recycle();
+ }
+ result = true;
+ } else if (mDroppingEvents) {
+ // If moves are small and we're already dropping events, continue dropping
+ result = true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mDroppingEvents) {
+ // Send a down event first, as we dropped a bunch of sudden jumps and assume that
+ // the user is releasing the touch on the second key.
+ MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
+ MotionEvent.ACTION_DOWN,
+ x, y, me.getMetaState());
+ super.onTouchEvent(translated);
+ translated.recycle();
+ mDroppingEvents = false;
+ // Let the up event get processed as well, result = false
+ }
+ break;
+ }
+ // Track the previous coordinate
+ mLastX = x;
+ mLastY = y;
+ return result;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ if (getLatinKeyboard() == null) return true;
+
+ // If there was a sudden jump, return without processing the actual motion event.
+ if (handleSuddenJump(me)) {
+ if (DEBUG_MODE)
+ Log.w(TAG, "onTouchEvent: ignore sudden jump " + me);
+ return true;
+ }
+
+ return super.onTouchEvent(me);
+ }
+
+ @Override
+ public void draw(Canvas c) {
+ Utils.GCUtils.getInstance().reset();
+ boolean tryGC = true;
+ for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
+ try {
+ super.draw(c);
+ tryGC = false;
+ } catch (OutOfMemoryError e) {
+ tryGC = Utils.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e);
+ }
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ // Token is available from here.
+ VoiceProxy.getInstance().onAttachedToWindow();
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
new file mode 100644
index 000000000..2d6766f2d
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
@@ -0,0 +1,52 @@
+/*
+ * 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.keyboard;
+
+import android.content.Context;
+
+import java.util.List;
+
+public class MiniKeyboard extends Keyboard {
+ private int mDefaultKeyCoordX;
+
+ public MiniKeyboard(Context context, int xmlLayoutResId, Keyboard parentKeyboard) {
+ super(context, xmlLayoutResId, null, parentKeyboard.getMinWidth());
+ }
+
+ public void setDefaultCoordX(int pos) {
+ mDefaultKeyCoordX = pos;
+ }
+
+ public int getDefaultCoordX() {
+ return mDefaultKeyCoordX;
+ }
+
+ public boolean isOneRowKeyboard() {
+ final List<Key> keys = getKeys();
+ if (keys.size() == 0) return false;
+ final int edgeFlags = keys.get(0).mEdgeFlags;
+ // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows,
+ // does not have both top and bottom edge flags on at the same time. On the other hand,
+ // the first key of mini keyboard that was created with popupCharacters must have both top
+ // and bottom edge flags on.
+ // When you want to use one row mini-keyboard from xml file, make sure that the row has
+ // both top and bottom edge flags set.
+ return (edgeFlags & Keyboard.EDGE_TOP) != 0
+ && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0;
+
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java b/java/src/com/android/inputmethod/keyboard/MiniKeyboardKeyDetector.java
index 356e62d48..cc5c3bbfe 100644
--- a/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/MiniKeyboardKeyDetector.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 Google Inc.
+ * 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
@@ -14,13 +14,11 @@
* the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.keyboard;
-import android.inputmethodservice.Keyboard.Key;
-
-class MiniKeyboardKeyDetector extends KeyDetector {
- private static final int MAX_NEARBY_KEYS = 1;
+import java.util.List;
+public class MiniKeyboardKeyDetector extends KeyDetector {
private final int mSlideAllowanceSquare;
private final int mSlideAllowanceSquareTop;
@@ -33,27 +31,29 @@ class MiniKeyboardKeyDetector extends KeyDetector {
@Override
protected int getMaxNearbyKeys() {
- return MAX_NEARBY_KEYS;
+ // No nearby key will be returned.
+ return 1;
}
@Override
- public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) {
- final Key[] keys = getKeys();
+ public int getKeyIndexAndNearbyCodes(int x, int y, final int[] allCodes) {
+ final List<Key> keys = getKeys();
final int touchX = getTouchX(x);
final int touchY = getTouchY(y);
- int closestKeyIndex = LatinKeyboardBaseView.NOT_A_KEY;
- int closestKeyDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
- final int keyCount = keys.length;
- for (int i = 0; i < keyCount; i++) {
- final Key key = keys[i];
- int dist = key.squaredDistanceFrom(touchX, touchY);
- if (dist < closestKeyDist) {
- closestKeyIndex = i;
- closestKeyDist = dist;
+
+ int nearestIndex = NOT_A_KEY;
+ int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
+ final int keyCount = keys.size();
+ for (int index = 0; index < keyCount; index++) {
+ final int dist = keys.get(index).squaredDistanceToEdge(touchX, touchY);
+ if (dist < nearestDist) {
+ nearestIndex = index;
+ nearestDist = dist;
}
}
- if (allKeys != null && closestKeyIndex != LatinKeyboardBaseView.NOT_A_KEY)
- allKeys[0] = keys[closestKeyIndex].codes[0];
- return closestKeyIndex;
+
+ if (allCodes != null && nearestIndex != NOT_A_KEY)
+ allCodes[0] = keys.get(nearestIndex).mCode;
+ return nearestIndex;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
new file mode 100644
index 000000000..c7620f946
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -0,0 +1,709 @@
+/*
+ * 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.keyboard;
+
+import android.content.res.Resources;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.keyboard.KeyboardView.UIHandler;
+import com.android.inputmethod.keyboard.internal.PointerTrackerKeyState;
+import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class PointerTracker {
+ private static final String TAG = PointerTracker.class.getSimpleName();
+ private static final boolean ENABLE_ASSERTION = false;
+ private static final boolean DEBUG_EVENT = false;
+ private static final boolean DEBUG_MOVE_EVENT = false;
+ private static final boolean DEBUG_LISTENER = false;
+ private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
+
+ public interface UIProxy {
+ public void invalidateKey(Key key);
+ public void showKeyPreview(int keyIndex, PointerTracker tracker);
+ public void dismissKeyPreview(PointerTracker tracker);
+ public boolean hasDistinctMultitouch();
+ }
+
+ public final int mPointerId;
+
+ // Timing constants
+ private final int mDelayBeforeKeyRepeatStart;
+ private final int mLongPressKeyTimeout;
+ private final int mLongPressShiftKeyTimeout;
+
+ private final KeyboardView mKeyboardView;
+ private final UIProxy mProxy;
+ private final UIHandler mHandler;
+ private final KeyDetector mKeyDetector;
+ private KeyboardActionListener mListener = EMPTY_LISTENER;
+ private final KeyboardSwitcher mKeyboardSwitcher;
+ private final boolean mHasDistinctMultitouch;
+ private final boolean mConfigSlidingKeyInputEnabled;
+
+ private final int mTouchNoiseThresholdMillis;
+ private final int mTouchNoiseThresholdDistanceSquared;
+
+ private Keyboard mKeyboard;
+ private List<Key> mKeys;
+ private int mKeyHysteresisDistanceSquared = -1;
+ private int mKeyQuarterWidthSquared;
+
+ private final PointerTrackerKeyState mKeyState;
+
+ // true if keyboard layout has been changed.
+ private boolean mKeyboardLayoutHasBeenChanged;
+
+ // true if event is already translated to a key action (long press or mini-keyboard)
+ private boolean mKeyAlreadyProcessed;
+
+ // true if this pointer is repeatable key
+ private boolean mIsRepeatableKey;
+
+ // true if this pointer is in sliding key input
+ private boolean mIsInSlidingKeyInput;
+
+ // true if sliding key is allowed.
+ private boolean mIsAllowedSlidingKeyInput;
+
+ // ignore modifier key if true
+ private boolean mIgnoreModifierKey;
+
+ // TODO: Remove these hacking variables
+ // true if this pointer is in sliding language switch
+ private boolean mIsInSlidingLanguageSwitch;
+ private int mSpaceKeyIndex;
+ private final SubtypeSwitcher mSubtypeSwitcher;
+
+ // Empty {@link KeyboardActionListener}
+ private static final KeyboardActionListener EMPTY_LISTENER = new KeyboardActionListener() {
+ @Override
+ public void onPress(int primaryCode, boolean withSliding) {}
+ @Override
+ public void onRelease(int primaryCode, boolean withSliding) {}
+ @Override
+ public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {}
+ @Override
+ public void onTextInput(CharSequence text) {}
+ @Override
+ public void onCancelInput() {}
+ @Override
+ public void onSwipeDown() {}
+ };
+
+ public PointerTracker(int id, KeyboardView keyboardView, UIHandler handler,
+ KeyDetector keyDetector, UIProxy proxy) {
+ if (proxy == null || handler == null || keyDetector == null)
+ throw new NullPointerException();
+ mPointerId = id;
+ mKeyboardView = keyboardView;
+ mProxy = proxy;
+ mHandler = handler;
+ mKeyDetector = keyDetector;
+ mKeyboardSwitcher = KeyboardSwitcher.getInstance();
+ mKeyState = new PointerTrackerKeyState(keyDetector);
+ mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
+ final Resources res = mKeyboardView.getResources();
+ mConfigSlidingKeyInputEnabled = res.getBoolean(R.bool.config_sliding_key_input_enabled);
+ mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
+ mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
+ mLongPressShiftKeyTimeout = res.getInteger(R.integer.config_long_press_shift_key_timeout);
+ mTouchNoiseThresholdMillis = res.getInteger(R.integer.config_touch_noise_threshold_millis);
+ final float touchNoiseThresholdDistance = res.getDimension(
+ R.dimen.config_touch_noise_threshold_distance);
+ mTouchNoiseThresholdDistanceSquared = (int)(
+ touchNoiseThresholdDistance * touchNoiseThresholdDistance);
+ mSubtypeSwitcher = SubtypeSwitcher.getInstance();
+ }
+
+ public void setOnKeyboardActionListener(KeyboardActionListener listener) {
+ mListener = listener;
+ }
+
+ // Returns true if keyboard has been changed by this callback.
+ private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key, boolean withSliding) {
+ final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
+ if (DEBUG_LISTENER)
+ Log.d(TAG, "onPress : " + keyCodePrintable(key.mCode) + " sliding=" + withSliding
+ + " ignoreModifier=" + ignoreModifierKey);
+ if (ignoreModifierKey)
+ return false;
+ if (key.isEnabled()) {
+ mListener.onPress(key.mCode, withSliding);
+ final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
+ mKeyboardLayoutHasBeenChanged = false;
+ return keyboardLayoutHasBeenChanged;
+ }
+ return false;
+ }
+
+ // Note that we need primaryCode argument because the keyboard may in shifted state and the
+ // primaryCode is different from {@link Key#mCode}.
+ private void callListenerOnCodeInput(Key key, int primaryCode, int[] keyCodes, int x, int y) {
+ final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
+ if (DEBUG_LISTENER)
+ Log.d(TAG, "onCodeInput: " + keyCodePrintable(primaryCode)
+ + " codes="+ Arrays.toString(keyCodes) + " x=" + x + " y=" + y
+ + " ignoreModifier=" + ignoreModifierKey);
+ if (ignoreModifierKey)
+ return;
+ if (key.isEnabled())
+ mListener.onCodeInput(primaryCode, keyCodes, x, y);
+ }
+
+ private void callListenerOnTextInput(Key key) {
+ if (DEBUG_LISTENER)
+ Log.d(TAG, "onTextInput: text=" + key.mOutputText);
+ if (key.isEnabled())
+ mListener.onTextInput(key.mOutputText);
+ }
+
+ // Note that we need primaryCode argument because the keyboard may in shifted state and the
+ // primaryCode is different from {@link Key#mCode}.
+ private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) {
+ final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
+ if (DEBUG_LISTENER)
+ Log.d(TAG, "onRelease : " + keyCodePrintable(primaryCode) + " sliding="
+ + withSliding + " ignoreModifier=" + ignoreModifierKey);
+ if (ignoreModifierKey)
+ return;
+ if (key.isEnabled())
+ mListener.onRelease(primaryCode, withSliding);
+ }
+
+ private void callListenerOnCancelInput() {
+ if (DEBUG_LISTENER)
+ Log.d(TAG, "onCancelInput");
+ mListener.onCancelInput();
+ }
+
+ public void setKeyboard(Keyboard keyboard, float keyHysteresisDistance) {
+ if (keyboard == null || keyHysteresisDistance < 0)
+ throw new IllegalArgumentException();
+ mKeyboard = keyboard;
+ mKeys = keyboard.getKeys();
+ mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
+ final int keyQuarterWidth = keyboard.getKeyWidth() / 4;
+ mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth;
+ // Mark that keyboard layout has been changed.
+ mKeyboardLayoutHasBeenChanged = true;
+ }
+
+ public boolean isInSlidingKeyInput() {
+ return mIsInSlidingKeyInput;
+ }
+
+ private boolean isValidKeyIndex(int keyIndex) {
+ return keyIndex >= 0 && keyIndex < mKeys.size();
+ }
+
+ public Key getKey(int keyIndex) {
+ return isValidKeyIndex(keyIndex) ? mKeys.get(keyIndex) : null;
+ }
+
+ private static boolean isModifierCode(int primaryCode) {
+ return primaryCode == Keyboard.CODE_SHIFT
+ || primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL;
+ }
+
+ private boolean isModifierInternal(int keyIndex) {
+ final Key key = getKey(keyIndex);
+ return key == null ? false : isModifierCode(key.mCode);
+ }
+
+ public boolean isModifier() {
+ return isModifierInternal(mKeyState.getKeyIndex());
+ }
+
+ private boolean isOnModifierKey(int x, int y) {
+ return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
+ }
+
+ public boolean isOnShiftKey(int x, int y) {
+ final Key key = getKey(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
+ return key != null && key.mCode == Keyboard.CODE_SHIFT;
+ }
+
+ public int getKeyIndexOn(int x, int y) {
+ return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
+ }
+
+ public boolean isSpaceKey(int keyIndex) {
+ Key key = getKey(keyIndex);
+ return key != null && key.mCode == Keyboard.CODE_SPACE;
+ }
+
+ public void setReleasedKeyGraphics() {
+ setReleasedKeyGraphics(mKeyState.getKeyIndex());
+ }
+
+ private void setReleasedKeyGraphics(int keyIndex) {
+ final Key key = getKey(keyIndex);
+ if (key != null) {
+ key.onReleased();
+ mProxy.invalidateKey(key);
+ }
+ }
+
+ private void setPressedKeyGraphics(int keyIndex) {
+ final Key key = getKey(keyIndex);
+ if (key != null && key.isEnabled()) {
+ key.onPressed();
+ mProxy.invalidateKey(key);
+ }
+ }
+
+ private void checkAssertion(PointerTrackerQueue queue) {
+ if (mHasDistinctMultitouch && queue == null)
+ throw new RuntimeException(
+ "PointerTrackerQueue must be passed on distinct multi touch device");
+ if (!mHasDistinctMultitouch && queue != null)
+ throw new RuntimeException(
+ "PointerTrackerQueue must be null on non-distinct multi touch device");
+ }
+
+ public void onTouchEvent(int action, int x, int y, long eventTime, PointerTrackerQueue queue) {
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ onMoveEvent(x, y, eventTime, queue);
+ break;
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ onDownEvent(x, y, eventTime, queue);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ onUpEvent(x, y, eventTime, queue);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ onCancelEvent(x, y, eventTime, queue);
+ break;
+ }
+ }
+
+ public void onDownEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
+ if (ENABLE_ASSERTION) checkAssertion(queue);
+ if (DEBUG_EVENT)
+ printTouchEvent("onDownEvent:", x, y, eventTime);
+
+ // Naive up-to-down noise filter.
+ final long deltaT = eventTime - mKeyState.getUpTime();
+ if (deltaT < mTouchNoiseThresholdMillis) {
+ final int dx = x - mKeyState.getLastX();
+ final int dy = y - mKeyState.getLastY();
+ final int distanceSquared = (dx * dx + dy * dy);
+ if (distanceSquared < mTouchNoiseThresholdDistanceSquared) {
+ if (DEBUG_MODE)
+ Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT
+ + " distance=" + distanceSquared);
+ mKeyAlreadyProcessed = true;
+ return;
+ }
+ }
+
+ if (queue != null) {
+ if (isOnModifierKey(x, y)) {
+ // Before processing a down event of modifier key, all pointers already being
+ // tracked should be released.
+ queue.releaseAllPointers(eventTime);
+ }
+ queue.add(this);
+ }
+ onDownEventInternal(x, y, eventTime);
+ }
+
+ private void onDownEventInternal(int x, int y, long eventTime) {
+ int keyIndex = mKeyState.onDownKey(x, y, eventTime);
+ // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
+ // from modifier key, or 3) this pointer is on mini-keyboard.
+ mIsAllowedSlidingKeyInput = mConfigSlidingKeyInputEnabled || isModifierInternal(keyIndex)
+ || mKeyDetector instanceof MiniKeyboardKeyDetector;
+ mKeyboardLayoutHasBeenChanged = false;
+ mKeyAlreadyProcessed = false;
+ mIsRepeatableKey = false;
+ mIsInSlidingKeyInput = false;
+ mIsInSlidingLanguageSwitch = false;
+ mIgnoreModifierKey = false;
+ if (isValidKeyIndex(keyIndex)) {
+ // This onPress call may have changed keyboard layout. Those cases are detected at
+ // {@link #setKeyboard}. In those cases, we should update keyIndex according to the new
+ // keyboard layout.
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), false))
+ keyIndex = mKeyState.onDownKey(x, y, eventTime);
+
+ startRepeatKey(keyIndex);
+ startLongPressTimer(keyIndex);
+ showKeyPreview(keyIndex);
+ setPressedKeyGraphics(keyIndex);
+ }
+ }
+
+ private void startSlidingKeyInput(Key key) {
+ if (!mIsInSlidingKeyInput)
+ mIgnoreModifierKey = isModifierCode(key.mCode);
+ mIsInSlidingKeyInput = true;
+ }
+
+ public void onMoveEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
+ if (ENABLE_ASSERTION) checkAssertion(queue);
+ if (DEBUG_MOVE_EVENT)
+ printTouchEvent("onMoveEvent:", x, y, eventTime);
+ if (mKeyAlreadyProcessed)
+ return;
+ final PointerTrackerKeyState keyState = mKeyState;
+
+ // TODO: Remove this hacking code
+ if (mIsInSlidingLanguageSwitch) {
+ ((LatinKeyboard)mKeyboard).updateSpacebarPreviewIcon(x - keyState.getKeyX());
+ showKeyPreview(mSpaceKeyIndex);
+ return;
+ }
+ final int lastX = keyState.getLastX();
+ final int lastY = keyState.getLastY();
+ final int oldKeyIndex = keyState.getKeyIndex();
+ final Key oldKey = getKey(oldKeyIndex);
+ int keyIndex = keyState.onMoveKey(x, y);
+ if (isValidKeyIndex(keyIndex)) {
+ if (oldKey == null) {
+ // The pointer has been slid in to the new key, but the finger was not on any keys.
+ // In this case, we must call onPress() to notify that the new key is being pressed.
+ // This onPress call may have changed keyboard layout. Those cases are detected at
+ // {@link #setKeyboard}. In those cases, we should update keyIndex according to the
+ // new keyboard layout.
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), true))
+ keyIndex = keyState.onMoveKey(x, y);
+ keyState.onMoveToNewKey(keyIndex, x, y);
+ startLongPressTimer(keyIndex);
+ showKeyPreview(keyIndex);
+ setPressedKeyGraphics(keyIndex);
+ } else if (isMajorEnoughMoveToBeOnNewKey(x, y, keyIndex)) {
+ // The pointer has been slid in to the new key from the previous key, we must call
+ // onRelease() first to notify that the previous key has been released, then call
+ // onPress() to notify that the new key is being pressed.
+ setReleasedKeyGraphics(oldKeyIndex);
+ callListenerOnRelease(oldKey, oldKey.mCode, true);
+ startSlidingKeyInput(oldKey);
+ mHandler.cancelKeyTimers();
+ startRepeatKey(keyIndex);
+ if (mIsAllowedSlidingKeyInput) {
+ // This onPress call may have changed keyboard layout. Those cases are detected
+ // at {@link #setKeyboard}. In those cases, we should update keyIndex according
+ // to the new keyboard layout.
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), true))
+ keyIndex = keyState.onMoveKey(x, y);
+ keyState.onMoveToNewKey(keyIndex, x, y);
+ startLongPressTimer(keyIndex);
+ setPressedKeyGraphics(keyIndex);
+ showKeyPreview(keyIndex);
+ } else {
+ // HACK: On some devices, quick successive touches may be translated to sudden
+ // move by touch panel firmware. This hack detects the case and translates the
+ // move event to successive up and down events.
+ final int dx = x - lastX;
+ final int dy = y - lastY;
+ final int lastMoveSquared = dx * dx + dy * dy;
+ if (lastMoveSquared >= mKeyQuarterWidthSquared) {
+ if (DEBUG_MODE)
+ Log.w(TAG, String.format("onMoveEvent: sudden move is translated to "
+ + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y));
+ onUpEventInternal(lastX, lastY, eventTime, true);
+ onDownEventInternal(x, y, eventTime);
+ } else {
+ mKeyAlreadyProcessed = true;
+ dismissKeyPreview();
+ setReleasedKeyGraphics(oldKeyIndex);
+ }
+ }
+ }
+ // TODO: Remove this hack code
+ else if (isSpaceKey(keyIndex) && !mIsInSlidingLanguageSwitch
+ && mKeyboard instanceof LatinKeyboard) {
+ final LatinKeyboard keyboard = ((LatinKeyboard)mKeyboard);
+ if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()
+ && mSubtypeSwitcher.getEnabledKeyboardLocaleCount() > 1) {
+ final int diff = x - keyState.getKeyX();
+ if (keyboard.shouldTriggerSpacebarSlidingLanguageSwitch(diff)) {
+ // Detect start sliding language switch.
+ mIsInSlidingLanguageSwitch = true;
+ mSpaceKeyIndex = keyIndex;
+ keyboard.updateSpacebarPreviewIcon(diff);
+ // Display spacebar slide language switcher.
+ showKeyPreview(keyIndex);
+ if (queue != null)
+ queue.releaseAllPointersExcept(this, eventTime, true);
+ }
+ }
+ }
+ } else {
+ if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, keyIndex)) {
+ // The pointer has been slid out from the previous key, we must call onRelease() to
+ // notify that the previous key has been released.
+ setReleasedKeyGraphics(oldKeyIndex);
+ callListenerOnRelease(oldKey, oldKey.mCode, true);
+ startSlidingKeyInput(oldKey);
+ mHandler.cancelLongPressTimers();
+ if (mIsAllowedSlidingKeyInput) {
+ keyState.onMoveToNewKey(keyIndex, x, y);
+ } else {
+ mKeyAlreadyProcessed = true;
+ dismissKeyPreview();
+ }
+ }
+ }
+ }
+
+ public void onUpEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
+ if (ENABLE_ASSERTION) checkAssertion(queue);
+ if (DEBUG_EVENT)
+ printTouchEvent("onUpEvent :", x, y, eventTime);
+
+ if (queue != null) {
+ if (isModifier()) {
+ // Before processing an up event of modifier key, all pointers already being
+ // tracked should be released.
+ queue.releaseAllPointersExcept(this, eventTime, true);
+ } else {
+ queue.releaseAllPointersOlderThan(this, eventTime);
+ }
+ queue.remove(this);
+ }
+ onUpEventInternal(x, y, eventTime, true);
+ }
+
+ // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
+ // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
+ // "virtual" up event.
+ public void onPhantomUpEvent(int x, int y, long eventTime, boolean updateReleasedKeyGraphics) {
+ if (DEBUG_EVENT)
+ printTouchEvent("onPhntEvent:", x, y, eventTime);
+ onUpEventInternal(x, y, eventTime, updateReleasedKeyGraphics);
+ mKeyAlreadyProcessed = true;
+ }
+
+ private void onUpEventInternal(int x, int y, long eventTime,
+ boolean updateReleasedKeyGraphics) {
+ mHandler.cancelKeyTimers();
+ mHandler.cancelShowKeyPreview(this);
+ mIsInSlidingKeyInput = false;
+ final PointerTrackerKeyState keyState = mKeyState;
+ final int keyX, keyY;
+ if (isMajorEnoughMoveToBeOnNewKey(x, y, keyState.onMoveKey(x, y))) {
+ keyX = x;
+ keyY = y;
+ } else {
+ // Use previous fixed key coordinates.
+ keyX = keyState.getKeyX();
+ keyY = keyState.getKeyY();
+ }
+ final int keyIndex = keyState.onUpKey(keyX, keyY, eventTime);
+ dismissKeyPreview();
+ if (updateReleasedKeyGraphics)
+ setReleasedKeyGraphics(keyIndex);
+ if (mKeyAlreadyProcessed)
+ return;
+ // TODO: Remove this hacking code
+ if (mIsInSlidingLanguageSwitch) {
+ setReleasedKeyGraphics(mSpaceKeyIndex);
+ final int languageDir = ((LatinKeyboard)mKeyboard).getLanguageChangeDirection();
+ if (languageDir != 0) {
+ final int code = (languageDir == 1)
+ ? LatinKeyboard.CODE_NEXT_LANGUAGE : LatinKeyboard.CODE_PREV_LANGUAGE;
+ // This will change keyboard layout.
+ mListener.onCodeInput(code, new int[] {code}, keyX, keyY);
+ }
+ mIsInSlidingLanguageSwitch = false;
+ ((LatinKeyboard)mKeyboard).setSpacebarSlidingLanguageSwitchDiff(0);
+ return;
+ }
+ if (!mIsRepeatableKey) {
+ detectAndSendKey(keyIndex, keyX, keyY);
+ }
+ }
+
+ public void onLongPressed(PointerTrackerQueue queue) {
+ mKeyAlreadyProcessed = true;
+ if (queue != null) {
+ // TODO: Support chording + long-press input.
+ queue.releaseAllPointersExcept(this, SystemClock.uptimeMillis(), true);
+ queue.remove(this);
+ }
+ }
+
+ public void onCancelEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
+ if (ENABLE_ASSERTION) checkAssertion(queue);
+ if (DEBUG_EVENT)
+ printTouchEvent("onCancelEvt:", x, y, eventTime);
+
+ if (queue != null) {
+ queue.releaseAllPointersExcept(this, eventTime, true);
+ queue.remove(this);
+ }
+ onCancelEventInternal();
+ }
+
+ private void onCancelEventInternal() {
+ mHandler.cancelKeyTimers();
+ mHandler.cancelShowKeyPreview(this);
+ dismissKeyPreview();
+ setReleasedKeyGraphics(mKeyState.getKeyIndex());
+ mIsInSlidingKeyInput = false;
+ }
+
+ private void startRepeatKey(int keyIndex) {
+ final Key key = getKey(keyIndex);
+ if (key != null && key.mRepeatable) {
+ dismissKeyPreview();
+ onRepeatKey(keyIndex);
+ mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
+ mIsRepeatableKey = true;
+ } else {
+ mIsRepeatableKey = false;
+ }
+ }
+
+ public void onRepeatKey(int keyIndex) {
+ Key key = getKey(keyIndex);
+ if (key != null) {
+ detectAndSendKey(keyIndex, key.mX, key.mY);
+ }
+ }
+
+ public int getLastX() {
+ return mKeyState.getLastX();
+ }
+
+ public int getLastY() {
+ return mKeyState.getLastY();
+ }
+
+ public long getDownTime() {
+ return mKeyState.getDownTime();
+ }
+
+ private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, int newKey) {
+ if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
+ throw new IllegalStateException("keyboard and/or hysteresis not set");
+ int curKey = mKeyState.getKeyIndex();
+ if (newKey == curKey) {
+ return false;
+ } else if (isValidKeyIndex(curKey)) {
+ return mKeys.get(curKey).squaredDistanceToEdge(x, y) >= mKeyHysteresisDistanceSquared;
+ } else {
+ return true;
+ }
+ }
+
+ // The modifier key, such as shift key, should not show its key preview.
+ private boolean isKeyPreviewNotRequired(int keyIndex) {
+ final Key key = getKey(keyIndex);
+ if (key == null || !key.isEnabled())
+ return true;
+ // Such as spacebar sliding language switch.
+ if (mKeyboard.needSpacebarPreview(keyIndex))
+ return false;
+ final int code = key.mCode;
+ return isModifierCode(code) || code == Keyboard.CODE_DELETE
+ || code == Keyboard.CODE_ENTER || code == Keyboard.CODE_SPACE;
+ }
+
+ private void showKeyPreview(int keyIndex) {
+ if (isKeyPreviewNotRequired(keyIndex))
+ return;
+ mProxy.showKeyPreview(keyIndex, this);
+ }
+
+ private void dismissKeyPreview() {
+ mProxy.dismissKeyPreview(this);
+ }
+
+ private void startLongPressTimer(int keyIndex) {
+ Key key = getKey(keyIndex);
+ if (key.mCode == Keyboard.CODE_SHIFT) {
+ mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this);
+ } else if (key.hasUppercaseLetter() && mKeyboard.isManualTemporaryUpperCase()) {
+ // We need not start long press timer on the key which has manual temporary upper case
+ // code defined and the keyboard is in manual temporary upper case mode.
+ return;
+ } else if (mKeyboardSwitcher.isInMomentarySwitchState()) {
+ // We use longer timeout for sliding finger input started from the symbols mode key.
+ mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this);
+ } else {
+ mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
+ }
+ }
+
+ private void detectAndSendKey(int index, int x, int y) {
+ final Key key = getKey(index);
+ if (key == null) {
+ callListenerOnCancelInput();
+ return;
+ }
+ if (key.mOutputText != null) {
+ callListenerOnTextInput(key);
+ callListenerOnRelease(key, key.mCode, false);
+ } else {
+ int code = key.mCode;
+ final int[] codes = mKeyDetector.newCodeArray();
+ mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
+
+ // If keyboard is in manual temporary upper case state and key has manual temporary
+ // uppercase letter as key hint letter, alternate character code should be sent.
+ if (mKeyboard.isManualTemporaryUpperCase() && key.hasUppercaseLetter()) {
+ code = key.mHintLetter.charAt(0);
+ codes[0] = code;
+ }
+
+ // Swap the first and second values in the codes array if the primary code is not the
+ // first value but the second value in the array. This happens when key debouncing is
+ // in effect.
+ if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
+ codes[1] = codes[0];
+ codes[0] = code;
+ }
+ callListenerOnCodeInput(key, code, codes, x, y);
+ callListenerOnRelease(key, code, false);
+ }
+ }
+
+ public CharSequence getPreviewText(Key key) {
+ return key.mLabel;
+ }
+
+ private long mPreviousEventTime;
+
+ private void printTouchEvent(String title, int x, int y, long eventTime) {
+ final int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
+ final Key key = getKey(keyIndex);
+ final String code = (key == null) ? "----" : keyCodePrintable(key.mCode);
+ final long delta = eventTime - mPreviousEventTime;
+ Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %3d(%s)", title,
+ (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, keyIndex, code));
+ mPreviousEventTime = eventTime;
+ }
+
+ private static String keyCodePrintable(int primaryCode) {
+ final String modifier = isModifierCode(primaryCode) ? " modifier" : "";
+ return String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode) + modifier;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java b/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java
new file mode 100644
index 000000000..3b8c36487
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java
@@ -0,0 +1,112 @@
+/*
+ * 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.keyboard;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.PopupWindow;
+
+import com.android.inputmethod.latin.R;
+
+/**
+ * A view that renders a virtual {@link MiniKeyboard}. It handles rendering of keys and detecting
+ * key presses and touch movements.
+ */
+public class PopupMiniKeyboardView extends KeyboardView implements PopupPanel {
+ private final int[] mCoordinates = new int[2];
+ private final boolean mConfigShowMiniKeyboardAtTouchedPoint;
+
+ private int mOriginX;
+ private int mOriginY;
+ private long mDownTime;
+
+ public PopupMiniKeyboardView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.popupMiniKeyboardViewStyle);
+ }
+
+ public PopupMiniKeyboardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final Resources res = context.getResources();
+ mConfigShowMiniKeyboardAtTouchedPoint = res.getBoolean(
+ R.bool.config_show_mini_keyboard_at_touched_point);
+ // Override default ProximityKeyDetector.
+ mKeyDetector = new MiniKeyboardKeyDetector(res.getDimension(
+ R.dimen.mini_keyboard_slide_allowance));
+ // Remove gesture detector on mini-keyboard
+ mGestureDetector = null;
+ setKeyPreviewPopupEnabled(false, 0);
+ }
+
+ @Override
+ public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
+ // Mini keyboard needs no pop-up key preview displayed, so we pass always false with a
+ // delay of 0. The delay does not matter actually since the popup is not shown anyway.
+ super.setKeyPreviewPopupEnabled(false, 0);
+ }
+
+ @Override
+ public void showPanel(KeyboardView parentKeyboardView, Key parentKey,
+ PointerTracker tracker, PopupWindow window) {
+ final View container = (View)getParent();
+ final MiniKeyboard miniKeyboard = (MiniKeyboard)getKeyboard();
+ final Keyboard parentKeyboard = parentKeyboardView.getKeyboard();
+
+ parentKeyboardView.getLocationInWindow(mCoordinates);
+ final int pointX = (mConfigShowMiniKeyboardAtTouchedPoint) ? tracker.getLastX()
+ : parentKey.mX + parentKey.mWidth / 2;
+ final int pointY = parentKey.mY;
+ final int miniKeyboardLeft = pointX - miniKeyboard.getDefaultCoordX()
+ + parentKeyboardView.getPaddingLeft();
+ final int x = Math.max(0, Math.min(miniKeyboardLeft,
+ parentKeyboardView.getWidth() - miniKeyboard.getMinWidth()))
+ - container.getPaddingLeft() + mCoordinates[0];
+ final int y = pointY - parentKeyboard.getVerticalGap()
+ - (container.getMeasuredHeight() - container.getPaddingBottom())
+ + parentKeyboardView.getPaddingTop() + mCoordinates[1];
+
+ if (miniKeyboard.setShifted(parentKeyboard.isShiftedOrShiftLocked())) {
+ invalidateAllKeys();
+ }
+ window.setContentView(container);
+ window.setWidth(container.getMeasuredWidth());
+ window.setHeight(container.getMeasuredHeight());
+ window.showAtLocation(parentKeyboardView, Gravity.NO_GRAVITY, x, y);
+
+ mOriginX = x + container.getPaddingLeft() - mCoordinates[0];
+ mOriginY = y + container.getPaddingTop() - mCoordinates[1];
+ mDownTime = SystemClock.uptimeMillis();
+
+ // Inject down event on the key to mini keyboard.
+ final MotionEvent downEvent = MotionEvent.obtain(mDownTime, mDownTime,
+ MotionEvent.ACTION_DOWN, pointX - mOriginX,
+ pointY + parentKey.mHeight / 2 - mOriginY, 0);
+ onTouchEvent(downEvent);
+ downEvent.recycle();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ me.offsetLocation(-mOriginX, -mOriginY);
+ return super.onTouchEvent(me);
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/PopupPanel.java b/java/src/com/android/inputmethod/keyboard/PopupPanel.java
new file mode 100644
index 000000000..386e11f2c
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/PopupPanel.java
@@ -0,0 +1,45 @@
+/*
+ * 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.keyboard;
+
+import android.view.MotionEvent;
+import android.widget.PopupWindow;
+
+public interface PopupPanel {
+ /**
+ * Show popup panel.
+ * @param parentKeyboardView the parent KeyboardView that has the parent key.
+ * @param parentKey the parent key that is the source of this popup panel
+ * @param tracker the pointer tracker that pressesd the parent key
+ * @param window PopupWindow to be used to show this popup panel
+ */
+ public void showPanel(KeyboardView parentKeyboardView, Key parentKey,
+ PointerTracker tracker, PopupWindow window);
+
+ /**
+ * Check if the pointer is in siding key input mode.
+ * @return true if the pointer is sliding key input mode.
+ */
+ public boolean isInSlidingKeyInput();
+
+ /**
+ * The motion event handler.
+ * @param me the MotionEvent to be processed.
+ * @return true if the motion event is processed and should be consumed.
+ */
+ public boolean onTouchEvent(MotionEvent me);
+}
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
new file mode 100644
index 000000000..33acc6907
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -0,0 +1,80 @@
+/*
+ * 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.keyboard;
+
+import com.android.inputmethod.latin.Utils;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ProximityInfo {
+ public static final int MAX_PROXIMITY_CHARS_SIZE = 16;
+
+ private final int mGridWidth;
+ private final int mGridHeight;
+ private final int mGridSize;
+
+ ProximityInfo(int gridWidth, int gridHeight) {
+ mGridWidth = gridWidth;
+ mGridHeight = gridHeight;
+ mGridSize = mGridWidth * mGridHeight;
+ }
+
+ private int mNativeProximityInfo;
+ static {
+ Utils.loadNativeLibrary();
+ }
+ private native int setProximityInfoNative(int maxProximityCharsSize, int displayWidth,
+ int displayHeight, int gridWidth, int gridHeight, int[] proximityCharsArray);
+ private native void releaseProximityInfoNative(int nativeProximityInfo);
+
+ public final void setProximityInfo(int[][] gridNeighborKeyIndexes, int keyboardWidth,
+ int keyboardHeight, List<Key> keys) {
+ int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE];
+ Arrays.fill(proximityCharsArray, KeyDetector.NOT_A_CODE);
+ for (int i = 0; i < mGridSize; ++i) {
+ final int proximityCharsLength = gridNeighborKeyIndexes[i].length;
+ for (int j = 0; j < proximityCharsLength; ++j) {
+ proximityCharsArray[i * MAX_PROXIMITY_CHARS_SIZE + j] =
+ keys.get(gridNeighborKeyIndexes[i][j]).mCode;
+ }
+ }
+ mNativeProximityInfo = setProximityInfoNative(MAX_PROXIMITY_CHARS_SIZE,
+ keyboardWidth, keyboardHeight, mGridWidth, mGridHeight, proximityCharsArray);
+ }
+
+ // TODO: Get rid of this function's input (keyboard).
+ public int getNativeProximityInfo(Keyboard keyboard) {
+ if (mNativeProximityInfo == 0) {
+ // TODO: Move this function to ProximityInfo and make this private.
+ keyboard.computeNearestNeighbors();
+ }
+ return mNativeProximityInfo;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mNativeProximityInfo != 0) {
+ releaseProximityInfoNative(mNativeProximityInfo);
+ mNativeProximityInfo = 0;
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
new file mode 100644
index 000000000..983f0649d
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
@@ -0,0 +1,241 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.internal.KeyboardParser.ParseException;
+import com.android.inputmethod.latin.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class KeyStyles {
+ private static final String TAG = "KeyStyles";
+ private static final boolean DEBUG = false;
+
+ private final HashMap<String, DeclaredKeyStyle> mStyles =
+ new HashMap<String, DeclaredKeyStyle>();
+ private static final KeyStyle EMPTY_KEY_STYLE = new EmptyKeyStyle();
+
+ public interface KeyStyle {
+ public CharSequence[] getTextArray(TypedArray a, int index);
+ public CharSequence getText(TypedArray a, int index);
+ public int getInt(TypedArray a, int index, int defaultValue);
+ public int getFlag(TypedArray a, int index, int defaultValue);
+ public boolean getBoolean(TypedArray a, int index, boolean defaultValue);
+ }
+
+ /* package */ static class EmptyKeyStyle implements KeyStyle {
+ private EmptyKeyStyle() {
+ // Nothing to do.
+ }
+
+ @Override
+ public CharSequence[] getTextArray(TypedArray a, int index) {
+ return parseTextArray(a, index);
+ }
+
+ @Override
+ public CharSequence getText(TypedArray a, int index) {
+ return a.getText(index);
+ }
+
+ @Override
+ public int getInt(TypedArray a, int index, int defaultValue) {
+ return a.getInt(index, defaultValue);
+ }
+
+ @Override
+ public int getFlag(TypedArray a, int index, int defaultValue) {
+ return a.getInt(index, defaultValue);
+ }
+
+ @Override
+ public boolean getBoolean(TypedArray a, int index, boolean defaultValue) {
+ return a.getBoolean(index, defaultValue);
+ }
+
+ protected static CharSequence[] parseTextArray(TypedArray a, int index) {
+ if (!a.hasValue(index))
+ return null;
+ final CharSequence text = a.getText(index);
+ return parseCsvText(text);
+ }
+
+ /* package */ static CharSequence[] parseCsvText(CharSequence text) {
+ final int size = text.length();
+ if (size == 0) return null;
+ if (size == 1) return new CharSequence[] { text };
+ final StringBuilder sb = new StringBuilder();
+ ArrayList<CharSequence> list = null;
+ int start = 0;
+ for (int pos = 0; pos < size; pos++) {
+ final char c = text.charAt(pos);
+ if (c == ',') {
+ if (list == null) list = new ArrayList<CharSequence>();
+ if (sb.length() == 0) {
+ list.add(text.subSequence(start, pos));
+ } else {
+ list.add(sb.toString());
+ sb.setLength(0);
+ }
+ start = pos + 1;
+ continue;
+ } else if (c == '\\') {
+ if (start == pos) {
+ // Skip escape character at the beginning of the value.
+ start++;
+ pos++;
+ } else {
+ if (start < pos && sb.length() == 0)
+ sb.append(text.subSequence(start, pos));
+ pos++;
+ if (pos < size)
+ sb.append(text.charAt(pos));
+ }
+ } else if (sb.length() > 0) {
+ sb.append(c);
+ }
+ }
+ if (list == null) {
+ return new CharSequence[] { sb.length() > 0 ? sb : text.subSequence(start, size) };
+ } else {
+ list.add(sb.length() > 0 ? sb : text.subSequence(start, size));
+ return list.toArray(new CharSequence[list.size()]);
+ }
+ }
+ }
+
+ private static class DeclaredKeyStyle extends EmptyKeyStyle {
+ private final HashMap<Integer, Object> mAttributes = new HashMap<Integer, Object>();
+
+ @Override
+ public CharSequence[] getTextArray(TypedArray a, int index) {
+ return a.hasValue(index)
+ ? super.getTextArray(a, index) : (CharSequence[])mAttributes.get(index);
+ }
+
+ @Override
+ public CharSequence getText(TypedArray a, int index) {
+ return a.hasValue(index)
+ ? super.getText(a, index) : (CharSequence)mAttributes.get(index);
+ }
+
+ @Override
+ public int getInt(TypedArray a, int index, int defaultValue) {
+ final Integer value = (Integer)mAttributes.get(index);
+ return super.getInt(a, index, (value != null) ? value : defaultValue);
+ }
+
+ @Override
+ public int getFlag(TypedArray a, int index, int defaultValue) {
+ final Integer value = (Integer)mAttributes.get(index);
+ return super.getFlag(a, index, defaultValue) | (value != null ? value : 0);
+ }
+
+ @Override
+ public boolean getBoolean(TypedArray a, int index, boolean defaultValue) {
+ final Boolean value = (Boolean)mAttributes.get(index);
+ return super.getBoolean(a, index, (value != null) ? value : defaultValue);
+ }
+
+ private DeclaredKeyStyle() {
+ super();
+ }
+
+ private void parseKeyStyleAttributes(TypedArray keyAttr) {
+ // TODO: Currently not all Key attributes can be declared as style.
+ readInt(keyAttr, R.styleable.Keyboard_Key_code);
+ readText(keyAttr, R.styleable.Keyboard_Key_keyLabel);
+ readText(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
+ readText(keyAttr, R.styleable.Keyboard_Key_keyHintLetter);
+ readTextArray(keyAttr, R.styleable.Keyboard_Key_popupCharacters);
+ readFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption);
+ readInt(keyAttr, R.styleable.Keyboard_Key_keyIcon);
+ readInt(keyAttr, R.styleable.Keyboard_Key_keyIconPreview);
+ readInt(keyAttr, R.styleable.Keyboard_Key_keyIconShifted);
+ readInt(keyAttr, R.styleable.Keyboard_Key_maxPopupKeyboardColumn);
+ readBoolean(keyAttr, R.styleable.Keyboard_Key_isFunctional);
+ readBoolean(keyAttr, R.styleable.Keyboard_Key_isSticky);
+ readBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable);
+ readBoolean(keyAttr, R.styleable.Keyboard_Key_enabled);
+ }
+
+ private void readText(TypedArray a, int index) {
+ if (a.hasValue(index))
+ mAttributes.put(index, a.getText(index));
+ }
+
+ private void readInt(TypedArray a, int index) {
+ if (a.hasValue(index))
+ mAttributes.put(index, a.getInt(index, 0));
+ }
+
+ private void readFlag(TypedArray a, int index) {
+ final Integer value = (Integer)mAttributes.get(index);
+ if (a.hasValue(index))
+ mAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0));
+ }
+
+ private void readBoolean(TypedArray a, int index) {
+ if (a.hasValue(index))
+ mAttributes.put(index, a.getBoolean(index, false));
+ }
+
+ private void readTextArray(TypedArray a, int index) {
+ final CharSequence[] value = parseTextArray(a, index);
+ if (value != null)
+ mAttributes.put(index, value);
+ }
+
+ private void addParent(DeclaredKeyStyle parentStyle) {
+ mAttributes.putAll(parentStyle.mAttributes);
+ }
+ }
+
+ public void parseKeyStyleAttributes(TypedArray keyStyleAttr, TypedArray keyAttrs,
+ XmlResourceParser parser) {
+ String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName);
+ if (DEBUG) Log.d(TAG, String.format("<%s styleName=%s />",
+ KeyboardParser.TAG_KEY_STYLE, styleName));
+ if (mStyles.containsKey(styleName))
+ throw new ParseException("duplicate key style declared: " + styleName, parser);
+
+ final DeclaredKeyStyle style = new DeclaredKeyStyle();
+ if (keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_parentStyle)) {
+ String parentStyle = keyStyleAttr.getString(
+ R.styleable.Keyboard_KeyStyle_parentStyle);
+ final DeclaredKeyStyle parent = mStyles.get(parentStyle);
+ if (parent == null)
+ throw new ParseException("Unknown parentStyle " + parent, parser);
+ style.addParent(parent);
+ }
+ style.parseKeyStyleAttributes(keyAttrs);
+ mStyles.put(styleName, style);
+ }
+
+ public KeyStyle getKeyStyle(String styleName) {
+ return mStyles.get(styleName);
+ }
+
+ public KeyStyle getEmptyKeyStyle() {
+ return EMPTY_KEY_STYLE;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
new file mode 100644
index 000000000..37b36825a
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -0,0 +1,146 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.R;
+
+public class KeyboardIconsSet {
+ private static final String TAG = KeyboardIconsSet.class.getSimpleName();
+
+ public static final int ICON_UNDEFINED = 0;
+
+ // This should be aligned with Keyboard.keyIcon enum.
+ private static final int ICON_SHIFT_KEY = 1;
+ private static final int ICON_TO_SYMBOL_KEY = 2;
+ private static final int ICON_TO_SYMBOL_KEY_WITH_SHORTCUT = 3;
+ private static final int ICON_DELETE_KEY = 4;
+ private static final int ICON_SETTINGS_KEY = 5;
+ private static final int ICON_SHORTCUT_KEY = 6;
+ private static final int ICON_SPACE_KEY = 7;
+ private static final int ICON_RETURN_KEY = 8;
+ private static final int ICON_SEARCH_KEY = 9;
+ private static final int ICON_TAB_KEY = 10;
+ private static final int ICON_NUM1_KEY = 11;
+ private static final int ICON_NUM2_KEY = 12;
+ private static final int ICON_NUM3_KEY = 13;
+ private static final int ICON_NUM4_KEY = 14;
+ private static final int ICON_NUM5_KEY = 15;
+ private static final int ICON_NUM6_KEY = 16;
+ private static final int ICON_NUM7_KEY = 17;
+ private static final int ICON_NUM8_KEY = 18;
+ private static final int ICON_NUM9_KEY = 19;
+ private static final int ICON_NUM0_KEY = 20;
+ // This should be aligned with Keyboard.keyIconShifted enum.
+ private static final int ICON_SHIFTED_SHIFT_KEY = 21;
+ // This should be aligned with Keyboard.keyIconPreview enum.
+ private static final int ICON_PREVIEW_SPACE_KEY = 22;
+ private static final int ICON_PREVIEW_TAB_KEY = 23;
+ private static final int ICON_PREVIEW_SETTINGS_KEY = 24;
+ private static final int ICON_PREVIEW_SHORTCUT_KEY = 25;
+
+ private static final int ICON_LAST = 25;
+
+ private final Drawable mIcons[] = new Drawable[ICON_LAST + 1];
+
+ private static final int getIconId(int attrIndex) {
+ switch (attrIndex) {
+ case R.styleable.Keyboard_iconShiftKey:
+ return ICON_SHIFT_KEY;
+ case R.styleable.Keyboard_iconToSymbolKey:
+ return ICON_TO_SYMBOL_KEY;
+ case R.styleable.Keyboard_iconToSymbolKeyWithShortcut:
+ return ICON_TO_SYMBOL_KEY_WITH_SHORTCUT;
+ case R.styleable.Keyboard_iconDeleteKey:
+ return ICON_DELETE_KEY;
+ case R.styleable.Keyboard_iconSettingsKey:
+ return ICON_SETTINGS_KEY;
+ case R.styleable.Keyboard_iconShortcutKey:
+ return ICON_SHORTCUT_KEY;
+ case R.styleable.Keyboard_iconSpaceKey:
+ return ICON_SPACE_KEY;
+ case R.styleable.Keyboard_iconReturnKey:
+ return ICON_RETURN_KEY;
+ case R.styleable.Keyboard_iconSearchKey:
+ return ICON_SEARCH_KEY;
+ case R.styleable.Keyboard_iconTabKey:
+ return ICON_TAB_KEY;
+ case R.styleable.Keyboard_iconNum1Key:
+ return ICON_NUM1_KEY;
+ case R.styleable.Keyboard_iconNum2Key:
+ return ICON_NUM2_KEY;
+ case R.styleable.Keyboard_iconNum3Key:
+ return ICON_NUM3_KEY;
+ case R.styleable.Keyboard_iconNum4Key:
+ return ICON_NUM4_KEY;
+ case R.styleable.Keyboard_iconNum5Key:
+ return ICON_NUM5_KEY;
+ case R.styleable.Keyboard_iconNum6Key:
+ return ICON_NUM6_KEY;
+ case R.styleable.Keyboard_iconNum7Key:
+ return ICON_NUM7_KEY;
+ case R.styleable.Keyboard_iconNum8Key:
+ return ICON_NUM8_KEY;
+ case R.styleable.Keyboard_iconNum9Key:
+ return ICON_NUM9_KEY;
+ case R.styleable.Keyboard_iconNum0Key:
+ return ICON_NUM0_KEY;
+ case R.styleable.Keyboard_iconShiftedShiftKey:
+ return ICON_SHIFTED_SHIFT_KEY;
+ case R.styleable.Keyboard_iconPreviewSpaceKey:
+ return ICON_PREVIEW_SPACE_KEY;
+ case R.styleable.Keyboard_iconPreviewTabKey:
+ return ICON_PREVIEW_TAB_KEY;
+ case R.styleable.Keyboard_iconPreviewSettingsKey:
+ return ICON_PREVIEW_SETTINGS_KEY;
+ case R.styleable.Keyboard_iconPreviewShortcutKey:
+ return ICON_PREVIEW_SHORTCUT_KEY;
+ default:
+ return ICON_UNDEFINED;
+ }
+ }
+
+ public void loadIcons(TypedArray keyboardAttrs) {
+ final int count = keyboardAttrs.getIndexCount();
+ for (int i = 0; i < count; i++) {
+ final int attrIndex = keyboardAttrs.getIndex(i);
+ final int iconId = getIconId(attrIndex);
+ if (iconId != ICON_UNDEFINED) {
+ try {
+ final Drawable icon = keyboardAttrs.getDrawable(attrIndex);
+ Keyboard.setDefaultBounds(icon);
+ mIcons[iconId] = icon;
+ } catch (Resources.NotFoundException e) {
+ Log.w(TAG, "Drawable resource for icon #" + iconId + " not found");
+ }
+ }
+ }
+ }
+
+ public Drawable getIcon(int iconId) {
+ if (iconId == ICON_UNDEFINED)
+ return null;
+ if (iconId < 0 || iconId >= mIcons.length)
+ throw new IllegalArgumentException("icon id is out of range: " + iconId);
+ return mIcons[iconId];
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java
new file mode 100644
index 000000000..a6708171f
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java
@@ -0,0 +1,726 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.InflateException;
+
+import com.android.inputmethod.compat.EditorInfoCompatUtils;
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parser for BaseKeyboard.
+ *
+ * This class parses Keyboard XML file and fill out keys in Keyboard.
+ * The Keyboard XML file looks like:
+ * <pre>
+ * &gt;!-- xml/keyboard.xml --&lt;
+ * &gt;Keyboard keyboard_attributes*&lt;
+ * &gt;!-- Keyboard Content --&lt;
+ * &gt;Row row_attributes*&lt;
+ * &gt;!-- Row Content --&lt;
+ * &gt;Key key_attributes* /&lt;
+ * &gt;Spacer horizontalGap="0.2in" /&lt;
+ * &gt;include keyboardLayout="@xml/other_keys"&lt;
+ * ...
+ * &gt;/Row&lt;
+ * &gt;include keyboardLayout="@xml/other_rows"&lt;
+ * ...
+ * &gt;/Keyboard&lt;
+ * </pre>
+ * The XML file which is included in other file must have &gt;merge&lt; as root element, such as:
+ * <pre>
+ * &gt;!-- xml/other_keys.xml --&lt;
+ * &gt;merge&lt;
+ * &gt;Key key_attributes* /&lt;
+ * ...
+ * &gt;/merge&lt;
+ * </pre>
+ * and
+ * <pre>
+ * &gt;!-- xml/other_rows.xml --&lt;
+ * &gt;merge&lt;
+ * &gt;Row row_attributes*&lt;
+ * &gt;Key key_attributes* /&lt;
+ * &gt;/Row&lt;
+ * ...
+ * &gt;/merge&lt;
+ * </pre>
+ * You can also use switch-case-default tags to select Rows and Keys.
+ * <pre>
+ * &gt;switch&lt;
+ * &gt;case case_attribute*&lt;
+ * &gt;!-- Any valid tags at switch position --&lt;
+ * &gt;/case&lt;
+ * ...
+ * &gt;default&lt;
+ * &gt;!-- Any valid tags at switch position --&lt;
+ * &gt;/default&lt;
+ * &gt;/switch&lt;
+ * </pre>
+ * You can declare Key style and specify styles within Key tags.
+ * <pre>
+ * &gt;switch&lt;
+ * &gt;case mode="email"&lt;
+ * &gt;key-style styleName="f1-key" parentStyle="modifier-key"
+ * keyLabel=".com"
+ * /&lt;
+ * &gt;/case&lt;
+ * &gt;case mode="url"&lt;
+ * &gt;key-style styleName="f1-key" parentStyle="modifier-key"
+ * keyLabel="http://"
+ * /&lt;
+ * &gt;/case&lt;
+ * &gt;/switch&lt;
+ * ...
+ * &gt;Key keyStyle="shift-key" ... /&lt;
+ * </pre>
+ */
+
+public class KeyboardParser {
+ private static final String TAG = KeyboardParser.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ // Keyboard XML Tags
+ private static final String TAG_KEYBOARD = "Keyboard";
+ private static final String TAG_ROW = "Row";
+ private static final String TAG_KEY = "Key";
+ private static final String TAG_SPACER = "Spacer";
+ private static final String TAG_INCLUDE = "include";
+ private static final String TAG_MERGE = "merge";
+ private static final String TAG_SWITCH = "switch";
+ private static final String TAG_CASE = "case";
+ private static final String TAG_DEFAULT = "default";
+ public static final String TAG_KEY_STYLE = "key-style";
+
+ private final Keyboard mKeyboard;
+ private final Context mContext;
+ private final Resources mResources;
+
+ private int mKeyboardTopPadding;
+ private int mKeyboardBottomPadding;
+ private int mHorizontalEdgesPadding;
+ private int mCurrentX = 0;
+ private int mCurrentY = 0;
+ private int mMaxRowWidth = 0;
+ private int mTotalHeight = 0;
+ private Row mCurrentRow = null;
+ private final KeyStyles mKeyStyles = new KeyStyles();
+
+ public KeyboardParser(Keyboard keyboard, Context context) {
+ mKeyboard = keyboard;
+ mContext = context;
+ final Resources res = context.getResources();
+ mResources = res;
+ mHorizontalEdgesPadding = (int)res.getDimension(R.dimen.keyboard_horizontal_edges_padding);
+ }
+
+ public int getMaxRowWidth() {
+ return mMaxRowWidth;
+ }
+
+ public int getTotalHeight() {
+ return mTotalHeight;
+ }
+
+ public void parseKeyboard(int resId) throws XmlPullParserException, IOException {
+ if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_KEYBOARD, mKeyboard.mId));
+ final XmlResourceParser parser = mResources.getXml(resId);
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEYBOARD.equals(tag)) {
+ parseKeyboardAttributes(parser);
+ startKeyboard();
+ parseKeyboardContent(parser, mKeyboard.getKeys());
+ break;
+ } else {
+ throw new IllegalStartTag(parser, TAG_KEYBOARD);
+ }
+ }
+ }
+ }
+
+ public static String parseKeyboardLocale(
+ Context context, int resId) throws XmlPullParserException, IOException {
+ final Resources res = context.getResources();
+ final XmlResourceParser parser = res.getXml(resId);
+ if (parser == null) return "";
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEYBOARD.equals(tag)) {
+ final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard);
+ return keyboardAttr.getString(R.styleable.Keyboard_keyboardLocale);
+ } else {
+ throw new IllegalStartTag(parser, TAG_KEYBOARD);
+ }
+ }
+ }
+ return "";
+ }
+
+ private void parseKeyboardAttributes(XmlResourceParser parser) {
+ final Keyboard keyboard = mKeyboard;
+ final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
+ Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
+ R.style.Keyboard);
+ final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Key);
+ try {
+ final int displayHeight = keyboard.getDisplayHeight();
+ final int keyboardHeight = (int)keyboardAttr.getDimension(
+ R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
+ final int maxKeyboardHeight = getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
+ int minKeyboardHeight = getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
+ if (minKeyboardHeight < 0) {
+ // Specified fraction was negative, so it should be calculated against display
+ // width.
+ final int displayWidth = keyboard.getDisplayWidth();
+ minKeyboardHeight = -getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
+ }
+ // Keyboard height will not exceed maxKeyboardHeight and will not be less than
+ // minKeyboardHeight.
+ final int height = Math.max(
+ Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
+ final int width = keyboard.getDisplayWidth();
+
+ keyboard.setKeyboardHeight(height);
+ keyboard.setKeyWidth(getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_keyWidth, width, width / 10));
+ keyboard.setRowHeight(getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_rowHeight, height, 50));
+ keyboard.setHorizontalGap(getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_horizontalGap, width, 0));
+ keyboard.setVerticalGap(getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_verticalGap, height, 0));
+ keyboard.setPopupKeyboardResId(keyboardAttr.getResourceId(
+ R.styleable.Keyboard_popupKeyboardTemplate, 0));
+
+ keyboard.setMaxPopupKeyboardColumn(keyAttr.getInt(
+ R.styleable.Keyboard_Key_maxPopupKeyboardColumn, 5));
+
+ mKeyboard.mIconsSet.loadIcons(keyboardAttr);
+ mKeyboardTopPadding = keyboardAttr.getDimensionPixelSize(
+ R.styleable.Keyboard_keyboardTopPadding, 0);
+ mKeyboardBottomPadding = keyboardAttr.getDimensionPixelSize(
+ R.styleable.Keyboard_keyboardBottomPadding, 0);
+ } finally {
+ keyAttr.recycle();
+ keyboardAttr.recycle();
+ }
+ }
+
+ private void parseKeyboardContent(XmlResourceParser parser, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_ROW.equals(tag)) {
+ Row row = new Row(mResources, mKeyboard, parser);
+ if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_ROW));
+ if (keys != null)
+ startRow(row);
+ parseRowContent(parser, row, keys);
+ } else if (TAG_INCLUDE.equals(tag)) {
+ parseIncludeKeyboardContent(parser, keys);
+ } else if (TAG_SWITCH.equals(tag)) {
+ parseSwitchKeyboardContent(parser, keys);
+ } else if (TAG_KEY_STYLE.equals(tag)) {
+ parseKeyStyle(parser, keys);
+ } else {
+ throw new IllegalStartTag(parser, TAG_ROW);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEYBOARD.equals(tag)) {
+ endKeyboard(mKeyboard.getVerticalGap());
+ break;
+ } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
+ || TAG_MERGE.equals(tag)) {
+ if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
+ break;
+ } else if (TAG_KEY_STYLE.equals(tag)) {
+ continue;
+ } else {
+ throw new IllegalEndTag(parser, TAG_ROW);
+ }
+ }
+ }
+ }
+
+ private void parseRowContent(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEY.equals(tag)) {
+ parseKey(parser, row, keys);
+ } else if (TAG_SPACER.equals(tag)) {
+ parseSpacer(parser, row, keys);
+ } else if (TAG_INCLUDE.equals(tag)) {
+ parseIncludeRowContent(parser, row, keys);
+ } else if (TAG_SWITCH.equals(tag)) {
+ parseSwitchRowContent(parser, row, keys);
+ } else if (TAG_KEY_STYLE.equals(tag)) {
+ parseKeyStyle(parser, keys);
+ } else {
+ throw new IllegalStartTag(parser, TAG_KEY);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (TAG_ROW.equals(tag)) {
+ if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_ROW));
+ if (keys != null)
+ endRow();
+ break;
+ } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
+ || TAG_MERGE.equals(tag)) {
+ if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
+ break;
+ } else if (TAG_KEY_STYLE.equals(tag)) {
+ continue;
+ } else {
+ throw new IllegalEndTag(parser, TAG_KEY);
+ }
+ }
+ }
+ }
+
+ private void parseKey(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ if (keys == null) {
+ checkEndTag(TAG_KEY, parser);
+ } else {
+ Key key = new Key(mResources, row, mCurrentX, mCurrentY, parser, mKeyStyles);
+ if (DEBUG) Log.d(TAG, String.format("<%s%s keyLabel=%s code=%d popupCharacters=%s />",
+ TAG_KEY, (key.isEnabled() ? "" : " disabled"), key.mLabel, key.mCode,
+ Arrays.toString(key.mPopupCharacters)));
+ checkEndTag(TAG_KEY, parser);
+ keys.add(key);
+ if (key.mCode == Keyboard.CODE_SHIFT)
+ mKeyboard.getShiftKeys().add(key);
+ endKey(key);
+ }
+ }
+
+ private void parseSpacer(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ if (keys == null) {
+ checkEndTag(TAG_SPACER, parser);
+ } else {
+ if (DEBUG) Log.d(TAG, String.format("<%s />", TAG_SPACER));
+ final TypedArray keyboardAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard);
+ if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap))
+ throw new IllegalAttribute(parser, "horizontalGap");
+ final int defaultWidth = (row != null) ? row.mDefaultWidth : 0;
+ final int keyWidth = getDimensionOrFraction(keyboardAttr, R.styleable.Keyboard_keyWidth,
+ mKeyboard.getDisplayWidth(), defaultWidth);
+ keyboardAttr.recycle();
+
+ final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Key);
+ int keyXPos = KeyboardParser.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyXPos, mKeyboard.getDisplayWidth(), mCurrentX);
+ if (keyXPos < 0) {
+ // If keyXPos is negative, the actual x-coordinate will be display_width + keyXPos.
+ keyXPos += mKeyboard.getDisplayWidth();
+ }
+
+ checkEndTag(TAG_SPACER, parser);
+ setSpacer(keyXPos, keyWidth);
+ }
+ }
+
+ private void parseIncludeKeyboardContent(XmlResourceParser parser, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ parseIncludeInternal(parser, null, keys);
+ }
+
+ private void parseIncludeRowContent(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ parseIncludeInternal(parser, row, keys);
+ }
+
+ private void parseIncludeInternal(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ if (keys == null) {
+ checkEndTag(TAG_INCLUDE, parser);
+ } else {
+ final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Include);
+ final int keyboardLayout = a.getResourceId(
+ R.styleable.Keyboard_Include_keyboardLayout, 0);
+ a.recycle();
+
+ checkEndTag(TAG_INCLUDE, parser);
+ if (keyboardLayout == 0)
+ throw new ParseException("No keyboardLayout attribute in <include/>", parser);
+ if (DEBUG) Log.d(TAG, String.format("<%s keyboardLayout=%s />",
+ TAG_INCLUDE, mResources.getResourceEntryName(keyboardLayout)));
+ parseMerge(mResources.getLayout(keyboardLayout), row, keys);
+ }
+ }
+
+ private void parseMerge(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_MERGE.equals(tag)) {
+ if (row == null) {
+ parseKeyboardContent(parser, keys);
+ } else {
+ parseRowContent(parser, row, keys);
+ }
+ break;
+ } else {
+ throw new ParseException(
+ "Included keyboard layout must have <merge> root element", parser);
+ }
+ }
+ }
+ }
+
+ private void parseSwitchKeyboardContent(XmlResourceParser parser, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ parseSwitchInternal(parser, null, keys);
+ }
+
+ private void parseSwitchRowContent(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ parseSwitchInternal(parser, row, keys);
+ }
+
+ private void parseSwitchInternal(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_SWITCH, mKeyboard.mId));
+ boolean selected = false;
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_CASE.equals(tag)) {
+ selected |= parseCase(parser, row, selected ? null : keys);
+ } else if (TAG_DEFAULT.equals(tag)) {
+ selected |= parseDefault(parser, row, selected ? null : keys);
+ } else {
+ throw new IllegalStartTag(parser, TAG_KEY);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (TAG_SWITCH.equals(tag)) {
+ if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_SWITCH));
+ break;
+ } else {
+ throw new IllegalEndTag(parser, TAG_KEY);
+ }
+ }
+ }
+ }
+
+ private boolean parseCase(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ final boolean selected = parseCaseCondition(parser);
+ if (row == null) {
+ // Processing Rows.
+ parseKeyboardContent(parser, selected ? keys : null);
+ } else {
+ // Processing Keys.
+ parseRowContent(parser, row, selected ? keys : null);
+ }
+ return selected;
+ }
+
+ private boolean parseCaseCondition(XmlResourceParser parser) {
+ final KeyboardId id = mKeyboard.mId;
+ if (id == null)
+ return true;
+
+ final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Case);
+ final TypedArray viewAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.KeyboardView);
+ try {
+ final boolean modeMatched = matchTypedValue(a,
+ R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
+ final boolean navigateActionMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_navigateAction, id.mNavigateAction);
+ final boolean passwordInputMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_passwordInput, id.mPasswordInput);
+ final boolean hasSettingsKeyMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_hasSettingsKey, id.mHasSettingsKey);
+ final boolean f2KeyModeMatched = matchInteger(a,
+ R.styleable.Keyboard_Case_f2KeyMode, id.mF2KeyMode);
+ final boolean clobberSettingsKeyMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
+ final boolean voiceEnabledMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_voiceKeyEnabled, id.mVoiceKeyEnabled);
+ final boolean voiceKeyMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_hasVoiceKey, id.mHasVoiceKey);
+ // As noted at {@link KeyboardId} class, we are interested only in enum value masked by
+ // {@link android.view.inputmethod.EditorInfo#IME_MASK_ACTION} and
+ // {@link android.view.inputmethod.EditorInfo#IME_FLAG_NO_ENTER_ACTION}. So matching
+ // this attribute with id.mImeOptions as integer value is enough for our purpose.
+ final boolean imeActionMatched = matchInteger(a,
+ R.styleable.Keyboard_Case_imeAction, id.mImeAction);
+ final boolean localeCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
+ final boolean languageCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
+ final boolean countryCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
+ final boolean selected = modeMatched && navigateActionMatched && passwordInputMatched
+ && hasSettingsKeyMatched && f2KeyModeMatched && clobberSettingsKeyMatched
+ && voiceEnabledMatched && voiceKeyMatched && imeActionMatched &&
+ localeCodeMatched && languageCodeMatched && countryCodeMatched;
+
+ if (DEBUG) Log.d(TAG, String.format("<%s%s%s%s%s%s%s%s%s%s%s%s%s> %s", TAG_CASE,
+ textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
+ booleanAttr(a, R.styleable.Keyboard_Case_navigateAction, "navigateAction"),
+ booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, "passwordInput"),
+ booleanAttr(a, R.styleable.Keyboard_Case_hasSettingsKey, "hasSettingsKey"),
+ textAttr(KeyboardId.f2KeyModeName(
+ a.getInt(R.styleable.Keyboard_Case_f2KeyMode, -1)), "f2KeyMode"),
+ booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
+ "clobberSettingsKey"),
+ booleanAttr(a, R.styleable.Keyboard_Case_voiceKeyEnabled, "voiceKeyEnabled"),
+ booleanAttr(a, R.styleable.Keyboard_Case_hasVoiceKey, "hasVoiceKey"),
+ textAttr(EditorInfoCompatUtils.imeOptionsName(
+ a.getInt(R.styleable.Keyboard_Case_imeAction, -1)), "imeAction"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), "localeCode"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), "languageCode"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), "countryCode"),
+ Boolean.toString(selected)));
+
+ return selected;
+ } finally {
+ a.recycle();
+ viewAttr.recycle();
+ }
+ }
+
+ private static boolean matchInteger(TypedArray a, int index, int value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for the
+ // attribute.
+ return !a.hasValue(index) || a.getInt(index, 0) == value;
+ }
+
+ private static boolean matchBoolean(TypedArray a, int index, boolean value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for the
+ // attribute.
+ return !a.hasValue(index) || a.getBoolean(index, false) == value;
+ }
+
+ private static boolean matchString(TypedArray a, int index, String value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for the
+ // attribute.
+ return !a.hasValue(index) || stringArrayContains(a.getString(index).split("\\|"), value);
+ }
+
+ private static boolean matchTypedValue(TypedArray a, int index, int intValue, String strValue) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for the
+ // attribute.
+ final TypedValue v = a.peekValue(index);
+ if (v == null)
+ return true;
+
+ if (isIntegerValue(v)) {
+ return intValue == a.getInt(index, 0);
+ } else if (isStringValue(v)) {
+ return stringArrayContains(a.getString(index).split("\\|"), strValue);
+ }
+ return false;
+ }
+
+ private static boolean stringArrayContains(String[] array, String value) {
+ for (final String elem : array) {
+ if (elem.equals(value))
+ return true;
+ }
+ return false;
+ }
+
+ private boolean parseDefault(XmlResourceParser parser, Row row, List<Key> keys)
+ throws XmlPullParserException, IOException {
+ if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_DEFAULT));
+ if (row == null) {
+ parseKeyboardContent(parser, keys);
+ } else {
+ parseRowContent(parser, row, keys);
+ }
+ return true;
+ }
+
+ private void parseKeyStyle(XmlResourceParser parser, List<Key> keys) {
+ TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_KeyStyle);
+ TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Key);
+ try {
+ if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
+ throw new ParseException("<" + TAG_KEY_STYLE
+ + "/> needs styleName attribute", parser);
+ if (keys != null)
+ mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
+ } finally {
+ keyStyleAttr.recycle();
+ keyAttrs.recycle();
+ }
+ }
+
+ private static void checkEndTag(String tag, XmlResourceParser parser)
+ throws XmlPullParserException, IOException {
+ if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName()))
+ return;
+ throw new NonEmptyTag(tag, parser);
+ }
+
+ private void startKeyboard() {
+ mCurrentY += mKeyboardTopPadding;
+ }
+
+ private void startRow(Row row) {
+ mCurrentX = 0;
+ setSpacer(mCurrentX, mHorizontalEdgesPadding);
+ mCurrentRow = row;
+ }
+
+ private void endRow() {
+ if (mCurrentRow == null)
+ throw new InflateException("orphant end row tag");
+ setSpacer(mCurrentX, mHorizontalEdgesPadding);
+ if (mCurrentX > mMaxRowWidth)
+ mMaxRowWidth = mCurrentX;
+ mCurrentY += mCurrentRow.mDefaultHeight;
+ mCurrentRow = null;
+ }
+
+ private void endKey(Key key) {
+ mCurrentX = key.mX - key.mGap / 2 + key.mWidth + key.mGap;
+ }
+
+ private void endKeyboard(int defaultVerticalGap) {
+ mCurrentY += mKeyboardBottomPadding;
+ mTotalHeight = mCurrentY - defaultVerticalGap;
+ }
+
+ private void setSpacer(int keyXPos, int width) {
+ mCurrentX = keyXPos + width;
+ }
+
+ public static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null)
+ return defValue;
+ if (isFractionValue(value)) {
+ // Round it to avoid values like 47.9999 from getting truncated
+ return Math.round(a.getFraction(index, base, base, defValue));
+ } else if (isDimensionValue(value)) {
+ return a.getDimensionPixelOffset(index, defValue);
+ } else if (isIntegerValue(value)) {
+ // For enum value.
+ return a.getInt(index, defValue);
+ }
+ return defValue;
+ }
+
+ private static boolean isFractionValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_FRACTION;
+ }
+
+ private static boolean isDimensionValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_DIMENSION;
+ }
+
+ private static boolean isIntegerValue(TypedValue v) {
+ return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
+ }
+
+ private static boolean isStringValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_STRING;
+ }
+
+ @SuppressWarnings("serial")
+ public static class ParseException extends InflateException {
+ public ParseException(String msg, XmlResourceParser parser) {
+ super(msg + " at line " + parser.getLineNumber());
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private static class IllegalStartTag extends ParseException {
+ public IllegalStartTag(XmlResourceParser parser, String parent) {
+ super("Illegal start tag " + parser.getName() + " in " + parent, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private static class IllegalEndTag extends ParseException {
+ public IllegalEndTag(XmlResourceParser parser, String parent) {
+ super("Illegal end tag " + parser.getName() + " in " + parent, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private static class IllegalAttribute extends ParseException {
+ public IllegalAttribute(XmlResourceParser parser, String attribute) {
+ super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private static class NonEmptyTag extends ParseException {
+ public NonEmptyTag(String tag, XmlResourceParser parser) {
+ super(tag + " must be empty tag", parser);
+ }
+ }
+
+ private static String textAttr(String value, String name) {
+ return value != null ? String.format(" %s=%s", name, value) : "";
+ }
+
+ private static String booleanAttr(TypedArray a, int index, String name) {
+ return a.hasValue(index) ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardShiftState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardShiftState.java
new file mode 100644
index 000000000..0cde4e5b5
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardShiftState.java
@@ -0,0 +1,135 @@
+/*
+ * 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.keyboard.internal;
+
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+
+public class KeyboardShiftState {
+ private static final String TAG = "KeyboardShiftState";
+ private static final boolean DEBUG = KeyboardSwitcher.DEBUG_STATE;
+
+ private static final int NORMAL = 0;
+ private static final int MANUAL_SHIFTED = 1;
+ private static final int MANUAL_SHIFTED_FROM_AUTO = 2;
+ private static final int AUTO_SHIFTED = 3;
+ private static final int SHIFT_LOCKED = 4;
+ private static final int SHIFT_LOCK_SHIFTED = 5;
+
+ private int mState = NORMAL;
+
+ public boolean setShifted(boolean newShiftState) {
+ final int oldState = mState;
+ if (newShiftState) {
+ switch (oldState) {
+ case NORMAL:
+ mState = MANUAL_SHIFTED;
+ break;
+ case AUTO_SHIFTED:
+ mState = MANUAL_SHIFTED_FROM_AUTO;
+ break;
+ case SHIFT_LOCKED:
+ mState = SHIFT_LOCK_SHIFTED;
+ break;
+ }
+ } else {
+ switch (oldState) {
+ case MANUAL_SHIFTED:
+ case MANUAL_SHIFTED_FROM_AUTO:
+ case AUTO_SHIFTED:
+ mState = NORMAL;
+ break;
+ case SHIFT_LOCK_SHIFTED:
+ mState = SHIFT_LOCKED;
+ break;
+ }
+ }
+ if (DEBUG)
+ Log.d(TAG, "setShifted(" + newShiftState + "): " + toString(oldState) + " > " + this);
+ return mState != oldState;
+ }
+
+ public void setShiftLocked(boolean newShiftLockState) {
+ final int oldState = mState;
+ if (newShiftLockState) {
+ switch (oldState) {
+ case NORMAL:
+ case MANUAL_SHIFTED:
+ case MANUAL_SHIFTED_FROM_AUTO:
+ case AUTO_SHIFTED:
+ mState = SHIFT_LOCKED;
+ break;
+ }
+ } else {
+ switch (oldState) {
+ case SHIFT_LOCKED:
+ case SHIFT_LOCK_SHIFTED:
+ mState = NORMAL;
+ break;
+ }
+ }
+ if (DEBUG)
+ Log.d(TAG, "setShiftLocked(" + newShiftLockState + "): " + toString(oldState)
+ + " > " + this);
+ }
+
+ public void setAutomaticTemporaryUpperCase() {
+ final int oldState = mState;
+ mState = AUTO_SHIFTED;
+ if (DEBUG)
+ Log.d(TAG, "setAutomaticTemporaryUpperCase: " + toString(oldState) + " > " + this);
+ }
+
+ public boolean isShiftedOrShiftLocked() {
+ return mState != NORMAL;
+ }
+
+ public boolean isShiftLocked() {
+ return mState == SHIFT_LOCKED || mState == SHIFT_LOCK_SHIFTED;
+ }
+
+ public boolean isAutomaticTemporaryUpperCase() {
+ return mState == AUTO_SHIFTED;
+ }
+
+ public boolean isManualTemporaryUpperCase() {
+ return mState == MANUAL_SHIFTED || mState == MANUAL_SHIFTED_FROM_AUTO
+ || mState == SHIFT_LOCK_SHIFTED;
+ }
+
+ public boolean isManualTemporaryUpperCaseFromAuto() {
+ return mState == MANUAL_SHIFTED_FROM_AUTO;
+ }
+
+ @Override
+ public String toString() {
+ return toString(mState);
+ }
+
+ private static String toString(int state) {
+ switch (state) {
+ case NORMAL: return "NORMAL";
+ case MANUAL_SHIFTED: return "MANUAL_SHIFTED";
+ case MANUAL_SHIFTED_FROM_AUTO: return "MANUAL_SHIFTED_FROM_AUTO";
+ case AUTO_SHIFTED: return "AUTO_SHIFTED";
+ case SHIFT_LOCKED: return "SHIFT_LOCKED";
+ case SHIFT_LOCK_SHIFTED: return "SHIFT_LOCK_SHIFTED";
+ default: return "UKNOWN";
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java
new file mode 100644
index 000000000..040c16ded
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java
@@ -0,0 +1,250 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.keyboard.MiniKeyboard;
+import com.android.inputmethod.latin.R;
+
+import java.util.List;
+
+public class MiniKeyboardBuilder {
+ private final Resources mRes;
+ private final MiniKeyboard mKeyboard;
+ private final CharSequence[] mPopupCharacters;
+ private final MiniKeyboardLayoutParams mParams;
+
+ /* package */ static class MiniKeyboardLayoutParams {
+ public final int mKeyWidth;
+ public final int mRowHeight;
+ /* package */ final int mTopRowAdjustment;
+ public final int mNumRows;
+ public final int mNumColumns;
+ public final int mLeftKeys;
+ public final int mRightKeys; // includes default key.
+
+ /**
+ * The object holding mini keyboard layout parameters.
+ *
+ * @param numKeys number of keys in this mini keyboard.
+ * @param maxColumns number of maximum columns of this mini keyboard.
+ * @param keyWidth mini keyboard key width in pixel, including horizontal gap.
+ * @param rowHeight mini keyboard row height in pixel, including vertical gap.
+ * @param coordXInParent coordinate x of the popup key in parent keyboard.
+ * @param parentKeyboardWidth parent keyboard width in pixel.
+ */
+ public MiniKeyboardLayoutParams(int numKeys, int maxColumns, int keyWidth, int rowHeight,
+ int coordXInParent, int parentKeyboardWidth) {
+ if (parentKeyboardWidth / keyWidth < maxColumns)
+ throw new IllegalArgumentException("Keyboard is too small to hold mini keyboard: "
+ + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
+ mKeyWidth = keyWidth;
+ mRowHeight = rowHeight;
+
+ final int numRows = (numKeys + maxColumns - 1) / maxColumns;
+ mNumRows = numRows;
+ final int numColumns = getOptimizedColumns(numKeys, maxColumns);
+ mNumColumns = numColumns;
+
+ final int numLeftKeys = (numColumns - 1) / 2;
+ final int numRightKeys = numColumns - numLeftKeys; // including default key.
+ final int maxLeftKeys = coordXInParent / keyWidth;
+ final int maxRightKeys = Math.max(1, (parentKeyboardWidth - coordXInParent) / keyWidth);
+ int leftKeys, rightKeys;
+ if (numLeftKeys > maxLeftKeys) {
+ leftKeys = maxLeftKeys;
+ rightKeys = numColumns - maxLeftKeys;
+ } else if (numRightKeys > maxRightKeys) {
+ leftKeys = numColumns - maxRightKeys;
+ rightKeys = maxRightKeys;
+ } else {
+ leftKeys = numLeftKeys;
+ rightKeys = numRightKeys;
+ }
+ // Shift right if the left edge of mini keyboard is on the edge of parent keyboard
+ // unless the parent key is on the left edge.
+ if (leftKeys * keyWidth >= coordXInParent && leftKeys > 0) {
+ leftKeys--;
+ rightKeys++;
+ }
+ // Shift left if the right edge of mini keyboard is on the edge of parent keyboard
+ // unless the parent key is on the right edge.
+ if (rightKeys * keyWidth + coordXInParent >= parentKeyboardWidth && rightKeys > 1) {
+ leftKeys++;
+ rightKeys--;
+ }
+ mLeftKeys = leftKeys;
+ mRightKeys = rightKeys;
+
+ // Centering of the top row.
+ final boolean onEdge = (leftKeys == 0 || rightKeys == 1);
+ if (numRows < 2 || onEdge || getTopRowEmptySlots(numKeys, numColumns) % 2 == 0) {
+ mTopRowAdjustment = 0;
+ } else if (mLeftKeys < mRightKeys - 1) {
+ mTopRowAdjustment = 1;
+ } else {
+ mTopRowAdjustment = -1;
+ }
+ }
+
+ // Return key position according to column count (0 is default).
+ /* package */ int getColumnPos(int n) {
+ final int col = n % mNumColumns;
+ if (col == 0) {
+ // default position.
+ return 0;
+ }
+ int pos = 0;
+ int right = 1; // include default position key.
+ int left = 0;
+ int i = 0;
+ while (true) {
+ // Assign right key if available.
+ if (right < mRightKeys) {
+ pos = right;
+ right++;
+ i++;
+ }
+ if (i >= col)
+ break;
+ // Assign left key if available.
+ if (left < mLeftKeys) {
+ left++;
+ pos = -left;
+ i++;
+ }
+ if (i >= col)
+ break;
+ }
+ return pos;
+ }
+
+ private static int getTopRowEmptySlots(int numKeys, int numColumns) {
+ final int remainingKeys = numKeys % numColumns;
+ if (remainingKeys == 0) {
+ return 0;
+ } else {
+ return numColumns - remainingKeys;
+ }
+ }
+
+ private int getOptimizedColumns(int numKeys, int maxColumns) {
+ int numColumns = Math.min(numKeys, maxColumns);
+ while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
+ numColumns--;
+ }
+ return numColumns;
+ }
+
+ public int getDefaultKeyCoordX() {
+ return mLeftKeys * mKeyWidth;
+ }
+
+ public int getX(int n, int row) {
+ final int x = getColumnPos(n) * mKeyWidth + getDefaultKeyCoordX();
+ if (isTopRow(row)) {
+ return x + mTopRowAdjustment * (mKeyWidth / 2);
+ }
+ return x;
+ }
+
+ public int getY(int row) {
+ return (mNumRows - 1 - row) * mRowHeight;
+ }
+
+ public int getRowFlags(int row) {
+ int rowFlags = 0;
+ if (row == 0) rowFlags |= Keyboard.EDGE_TOP;
+ if (isTopRow(row)) rowFlags |= Keyboard.EDGE_BOTTOM;
+ return rowFlags;
+ }
+
+ private boolean isTopRow(int rowCount) {
+ return rowCount == mNumRows - 1;
+ }
+ }
+
+ public MiniKeyboardBuilder(KeyboardView view, int layoutTemplateResId, Key parentKey,
+ Keyboard parentKeyboard) {
+ final Context context = view.getContext();
+ mRes = context.getResources();
+ final MiniKeyboard keyboard = new MiniKeyboard(
+ context, layoutTemplateResId, parentKeyboard);
+ mKeyboard = keyboard;
+ mPopupCharacters = parentKey.mPopupCharacters;
+
+ final int keyWidth = getMaxKeyWidth(view, mPopupCharacters, keyboard.getKeyWidth());
+ final MiniKeyboardLayoutParams params = new MiniKeyboardLayoutParams(
+ mPopupCharacters.length, parentKey.mMaxPopupColumn,
+ keyWidth, parentKeyboard.getRowHeight(),
+ parentKey.mX + (parentKey.mWidth + parentKey.mGap) / 2 - keyWidth / 2,
+ view.getMeasuredWidth());
+ mParams = params;
+
+ keyboard.setRowHeight(params.mRowHeight);
+ keyboard.setHeight(params.mNumRows * params.mRowHeight);
+ keyboard.setMinWidth(params.mNumColumns * params.mKeyWidth);
+ keyboard.setDefaultCoordX(params.getDefaultKeyCoordX() + params.mKeyWidth / 2);
+ }
+
+ private static int getMaxKeyWidth(KeyboardView view, CharSequence[] popupCharacters,
+ int minKeyWidth) {
+ Paint paint = null;
+ Rect bounds = null;
+ int maxWidth = 0;
+ for (CharSequence popupSpec : popupCharacters) {
+ final CharSequence label = PopupCharactersParser.getLabel(popupSpec.toString());
+ // If the label is single letter, minKeyWidth is enough to hold the label.
+ if (label != null && label.length() > 1) {
+ if (paint == null) {
+ paint = new Paint();
+ paint.setAntiAlias(true);
+ }
+ final int labelSize = view.getLabelSizeAndSetPaint(label, 0, paint);
+ paint.setTextSize(labelSize);
+ if (bounds == null) bounds = new Rect();
+ paint.getTextBounds(label.toString(), 0, label.length(), bounds);
+ if (maxWidth < bounds.width())
+ maxWidth = bounds.width();
+ }
+ }
+ final int horizontalPadding = (int)view.getContext().getResources().getDimension(
+ R.dimen.mini_keyboard_key_horizontal_padding);
+ return Math.max(minKeyWidth, maxWidth + horizontalPadding);
+ }
+
+ public MiniKeyboard build() {
+ final MiniKeyboard keyboard = mKeyboard;
+ final List<Key> keys = keyboard.getKeys();
+ final MiniKeyboardLayoutParams params = mParams;
+ for (int n = 0; n < mPopupCharacters.length; n++) {
+ final CharSequence label = mPopupCharacters[n];
+ final int row = n / params.mNumColumns;
+ final Key key = new Key(mRes, keyboard, label, params.getX(n, row), params.getY(row),
+ params.mKeyWidth, params.mRowHeight, params.getRowFlags(row));
+ keys.add(key);
+ }
+ return keyboard;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java b/java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java
new file mode 100644
index 000000000..dae73c4e4
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java
@@ -0,0 +1,85 @@
+/*
+ * 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.keyboard.internal;
+
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+
+public class ModifierKeyState {
+ protected static final String TAG = "ModifierKeyState";
+ protected static final boolean DEBUG = KeyboardSwitcher.DEBUG_STATE;
+
+ protected static final int RELEASING = 0;
+ protected static final int PRESSING = 1;
+ protected static final int MOMENTARY = 2;
+
+ protected final String mName;
+ protected int mState = RELEASING;
+
+ public ModifierKeyState(String name) {
+ mName = name;
+ }
+
+ public void onPress() {
+ final int oldState = mState;
+ mState = PRESSING;
+ if (DEBUG)
+ Log.d(TAG, mName + ".onPress: " + toString(oldState) + " > " + this);
+ }
+
+ public void onRelease() {
+ final int oldState = mState;
+ mState = RELEASING;
+ if (DEBUG)
+ Log.d(TAG, mName + ".onRelease: " + toString(oldState) + " > " + this);
+ }
+
+ public void onOtherKeyPressed() {
+ final int oldState = mState;
+ if (oldState == PRESSING)
+ mState = MOMENTARY;
+ if (DEBUG)
+ Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this);
+ }
+
+ public boolean isPressing() {
+ return mState == PRESSING;
+ }
+
+ public boolean isReleasing() {
+ return mState == RELEASING;
+ }
+
+ public boolean isMomentary() {
+ return mState == MOMENTARY;
+ }
+
+ @Override
+ public String toString() {
+ return toString(mState);
+ }
+
+ protected String toString(int state) {
+ switch (state) {
+ case RELEASING: return "RELEASING";
+ case PRESSING: return "PRESSING";
+ case MOMENTARY: return "MOMENTARY";
+ default: return "UNKNOWN";
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerKeyState.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerKeyState.java
new file mode 100644
index 000000000..ddadb1338
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerKeyState.java
@@ -0,0 +1,101 @@
+/*
+ * 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.keyboard.internal;
+
+import com.android.inputmethod.keyboard.KeyDetector;
+import com.android.inputmethod.keyboard.PointerTracker;
+
+/**
+ * This class keeps track of a key index and a position where {@link PointerTracker} is.
+ */
+public class PointerTrackerKeyState {
+ private final KeyDetector mKeyDetector;
+
+ // The position and time at which first down event occurred.
+ private long mDownTime;
+ private long mUpTime;
+
+ // The current key index where this pointer is.
+ private int mKeyIndex = KeyDetector.NOT_A_KEY;
+ // The position where mKeyIndex was recognized for the first time.
+ private int mKeyX;
+ private int mKeyY;
+
+ // Last pointer position.
+ private int mLastX;
+ private int mLastY;
+
+ public PointerTrackerKeyState(KeyDetector keyDetecor) {
+ mKeyDetector = keyDetecor;
+ }
+
+ public int getKeyIndex() {
+ return mKeyIndex;
+ }
+
+ public int getKeyX() {
+ return mKeyX;
+ }
+
+ public int getKeyY() {
+ return mKeyY;
+ }
+
+ public long getDownTime() {
+ return mDownTime;
+ }
+
+ public long getUpTime() {
+ return mUpTime;
+ }
+
+ public int getLastX() {
+ return mLastX;
+ }
+
+ public int getLastY() {
+ return mLastY;
+ }
+
+ public int onDownKey(int x, int y, long eventTime) {
+ mDownTime = eventTime;
+ return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
+ }
+
+ private int onMoveKeyInternal(int x, int y) {
+ mLastX = x;
+ mLastY = y;
+ return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
+ }
+
+ public int onMoveKey(int x, int y) {
+ return onMoveKeyInternal(x, y);
+ }
+
+ public int onMoveToNewKey(int keyIndex, int x, int y) {
+ mKeyIndex = keyIndex;
+ mKeyX = x;
+ mKeyY = y;
+ return keyIndex;
+ }
+
+ public int onUpKey(int x, int y, long eventTime) {
+ mUpTime = eventTime;
+ mKeyIndex = KeyDetector.NOT_A_KEY;
+ return onMoveKeyInternal(x, y);
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
new file mode 100644
index 000000000..f87cd869e
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -0,0 +1,85 @@
+/*
+ * 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.keyboard.internal;
+
+import com.android.inputmethod.keyboard.PointerTracker;
+
+import java.util.LinkedList;
+
+public class PointerTrackerQueue {
+ private LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
+
+ public void add(PointerTracker tracker) {
+ mQueue.add(tracker);
+ }
+
+ public void releaseAllPointersOlderThan(PointerTracker tracker, long eventTime) {
+ if (mQueue.lastIndexOf(tracker) < 0) {
+ return;
+ }
+ final LinkedList<PointerTracker> queue = mQueue;
+ int oldestPos = 0;
+ for (PointerTracker t = queue.get(oldestPos); t != tracker; t = queue.get(oldestPos)) {
+ if (t.isModifier()) {
+ oldestPos++;
+ } else {
+ t.onPhantomUpEvent(t.getLastX(), t.getLastY(), eventTime, true);
+ queue.remove(oldestPos);
+ }
+ }
+ }
+
+ public void releaseAllPointers(long eventTime) {
+ releaseAllPointersExcept(null, eventTime, true);
+ }
+
+ public void releaseAllPointersExcept(PointerTracker tracker, long eventTime,
+ boolean updateReleasedKeyGraphics) {
+ for (PointerTracker t : mQueue) {
+ if (t == tracker)
+ continue;
+ t.onPhantomUpEvent(t.getLastX(), t.getLastY(), eventTime, updateReleasedKeyGraphics);
+ }
+ mQueue.clear();
+ if (tracker != null)
+ mQueue.add(tracker);
+ }
+
+ public void remove(PointerTracker tracker) {
+ mQueue.remove(tracker);
+ }
+
+ public boolean isInSlidingKeyInput() {
+ for (final PointerTracker tracker : mQueue) {
+ if (tracker.isInSlidingKeyInput())
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("[");
+ for (PointerTracker tracker : mQueue) {
+ if (sb.length() > 1)
+ sb.append(" ");
+ sb.append(String.format("%d", tracker.mPointerId));
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PopupCharactersParser.java b/java/src/com/android/inputmethod/keyboard/internal/PopupCharactersParser.java
new file mode 100644
index 000000000..8276f5d78
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/PopupCharactersParser.java
@@ -0,0 +1,185 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.R;
+
+/**
+ * String parser of popupCharacters attribute of Key.
+ * The string is comma separated texts each of which represents one popup key.
+ * Each popup key text is one of the following:
+ * - A single letter (Letter)
+ * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
+ * - Icon followed by keyOutputText or code (@icon/icon_number|@integer/key_code)
+ * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\'
+ * character.
+ * Note that the character '@' and '\' are also parsed by XML parser and CSV parser as well.
+ * See {@link KeyboardIconsSet} about icon_number.
+ */
+public class PopupCharactersParser {
+ private static final String TAG = PopupCharactersParser.class.getSimpleName();
+
+ private static final char ESCAPE = '\\';
+ private static final String LABEL_END = "|";
+ private static final String PREFIX_AT = "@";
+ private static final String PREFIX_ICON = PREFIX_AT + "icon/";
+ private static final String PREFIX_CODE = PREFIX_AT + "integer/";
+
+ private PopupCharactersParser() {
+ // Intentional empty constructor for utility class.
+ }
+
+ private static boolean hasIcon(String popupSpec) {
+ if (popupSpec.startsWith(PREFIX_ICON)) {
+ final int end = indexOfLabelEnd(popupSpec, 0);
+ if (end > 0)
+ return true;
+ throw new PopupCharactersParserError("outputText or code not specified: " + popupSpec);
+ }
+ return false;
+ }
+
+ private static boolean hasCode(String popupSpec) {
+ final int end = indexOfLabelEnd(popupSpec, 0);
+ if (end > 0 && end + 1 < popupSpec.length()
+ && popupSpec.substring(end + 1).startsWith(PREFIX_CODE)) {
+ return true;
+ }
+ return false;
+ }
+
+ private static String parseEscape(String text) {
+ if (text.indexOf(ESCAPE) < 0)
+ return text;
+ final int length = text.length();
+ final StringBuilder sb = new StringBuilder();
+ for (int pos = 0; pos < length; pos++) {
+ final char c = text.charAt(pos);
+ if (c == ESCAPE && pos + 1 < length) {
+ sb.append(text.charAt(++pos));
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static int indexOfLabelEnd(String popupSpec, int start) {
+ if (popupSpec.indexOf(ESCAPE, start) < 0) {
+ final int end = popupSpec.indexOf(LABEL_END, start);
+ if (end == 0)
+ throw new PopupCharactersParserError(LABEL_END + " at " + start + ": " + popupSpec);
+ return end;
+ }
+ final int length = popupSpec.length();
+ for (int pos = start; pos < length; pos++) {
+ final char c = popupSpec.charAt(pos);
+ if (c == ESCAPE && pos + 1 < length) {
+ pos++;
+ } else if (popupSpec.startsWith(LABEL_END, pos)) {
+ return pos;
+ }
+ }
+ return -1;
+ }
+
+ public static String getLabel(String popupSpec) {
+ if (hasIcon(popupSpec))
+ return null;
+ final int end = indexOfLabelEnd(popupSpec, 0);
+ final String label = (end > 0) ? parseEscape(popupSpec.substring(0, end))
+ : parseEscape(popupSpec);
+ if (TextUtils.isEmpty(label))
+ throw new PopupCharactersParserError("Empty label: " + popupSpec);
+ return label;
+ }
+
+ public static String getOutputText(String popupSpec) {
+ if (hasCode(popupSpec))
+ return null;
+ final int end = indexOfLabelEnd(popupSpec, 0);
+ if (end > 0) {
+ if (indexOfLabelEnd(popupSpec, end + 1) >= 0)
+ throw new PopupCharactersParserError("Multiple " + LABEL_END + ": "
+ + popupSpec);
+ final String outputText = parseEscape(popupSpec.substring(end + LABEL_END.length()));
+ if (!TextUtils.isEmpty(outputText))
+ return outputText;
+ throw new PopupCharactersParserError("Empty outputText: " + popupSpec);
+ }
+ final String label = getLabel(popupSpec);
+ if (label == null)
+ throw new PopupCharactersParserError("Empty label: " + popupSpec);
+ // Code is automatically generated for one letter label. See {@link getCode()}.
+ if (label.length() == 1)
+ return null;
+ return label;
+ }
+
+ public static int getCode(Resources res, String popupSpec) {
+ if (hasCode(popupSpec)) {
+ final int end = indexOfLabelEnd(popupSpec, 0);
+ if (indexOfLabelEnd(popupSpec, end + 1) >= 0)
+ throw new PopupCharactersParserError("Multiple " + LABEL_END + ": " + popupSpec);
+ final int resId = getResourceId(res,
+ popupSpec.substring(end + LABEL_END.length() + PREFIX_AT.length()));
+ final int code = res.getInteger(resId);
+ return code;
+ }
+ if (indexOfLabelEnd(popupSpec, 0) > 0)
+ return Keyboard.CODE_DUMMY;
+ final String label = getLabel(popupSpec);
+ // Code is automatically generated for one letter label.
+ if (label != null && label.length() == 1)
+ return label.charAt(0);
+ return Keyboard.CODE_DUMMY;
+ }
+
+ public static int getIconId(String popupSpec) {
+ if (hasIcon(popupSpec)) {
+ int end = popupSpec.indexOf(LABEL_END, PREFIX_ICON.length() + 1);
+ final String iconId = popupSpec.substring(PREFIX_ICON.length(), end);
+ try {
+ return Integer.valueOf(iconId);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "illegal icon id specified: " + iconId);
+ return KeyboardIconsSet.ICON_UNDEFINED;
+ }
+ }
+ return KeyboardIconsSet.ICON_UNDEFINED;
+ }
+
+ private static int getResourceId(Resources res, String name) {
+ String packageName = res.getResourcePackageName(R.string.english_ime_name);
+ int resId = res.getIdentifier(name, null, packageName);
+ if (resId == 0)
+ throw new PopupCharactersParserError("Unknown resource: " + name);
+ return resId;
+ }
+
+ @SuppressWarnings("serial")
+ public static class PopupCharactersParserError extends RuntimeException {
+ public PopupCharactersParserError(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/Row.java b/java/src/com/android/inputmethod/keyboard/internal/Row.java
new file mode 100644
index 000000000..06aadcc05
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/Row.java
@@ -0,0 +1,73 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.Xml;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.R;
+
+/**
+ * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
+ * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
+ * defines.
+ */
+public class Row {
+ /** Default width of a key in this row. */
+ public final int mDefaultWidth;
+ /** Default height of a key in this row. */
+ public final int mDefaultHeight;
+ /** Default horizontal gap between keys in this row. */
+ public final int mDefaultHorizontalGap;
+ /** Vertical gap following this row. */
+ public final int mVerticalGap;
+ /**
+ * Edge flags for this row of keys. Possible values that can be assigned are
+ * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
+ */
+ public final int mRowEdgeFlags;
+
+ private final Keyboard mKeyboard;
+
+ public Row(Resources res, Keyboard keyboard, XmlResourceParser parser) {
+ this.mKeyboard = keyboard;
+ final int keyboardWidth = keyboard.getDisplayWidth();
+ final int keyboardHeight = keyboard.getKeyboardHeight();
+ TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard);
+ mDefaultWidth = KeyboardParser.getDimensionOrFraction(a,
+ R.styleable.Keyboard_keyWidth, keyboardWidth, keyboard.getKeyWidth());
+ mDefaultHeight = KeyboardParser.getDimensionOrFraction(a,
+ R.styleable.Keyboard_rowHeight, keyboardHeight, keyboard.getRowHeight());
+ mDefaultHorizontalGap = KeyboardParser.getDimensionOrFraction(a,
+ R.styleable.Keyboard_horizontalGap, keyboardWidth, keyboard.getHorizontalGap());
+ mVerticalGap = KeyboardParser.getDimensionOrFraction(a,
+ R.styleable.Keyboard_verticalGap, keyboardHeight, keyboard.getVerticalGap());
+ a.recycle();
+ a = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Row);
+ mRowEdgeFlags = a.getInt(R.styleable.Keyboard_Row_rowEdgeFlags, 0);
+ a.recycle();
+ }
+
+ public Keyboard getKeyboard() {
+ return mKeyboard;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java b/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java
new file mode 100644
index 000000000..6617b917f
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java
@@ -0,0 +1,69 @@
+/*
+ * 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.keyboard.internal;
+
+import android.util.Log;
+
+public class ShiftKeyState extends ModifierKeyState {
+ private static final int PRESSING_ON_SHIFTED = 3; // both temporary shifted & shift locked
+ private static final int IGNORING = 4;
+
+ public ShiftKeyState(String name) {
+ super(name);
+ }
+
+ @Override
+ public void onOtherKeyPressed() {
+ int oldState = mState;
+ if (oldState == PRESSING) {
+ mState = MOMENTARY;
+ } else if (oldState == PRESSING_ON_SHIFTED) {
+ mState = IGNORING;
+ }
+ if (DEBUG)
+ Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this);
+ }
+
+ public void onPressOnShifted() {
+ int oldState = mState;
+ mState = PRESSING_ON_SHIFTED;
+ if (DEBUG)
+ Log.d(TAG, mName + ".onPressOnShifted: " + toString(oldState) + " > " + this);
+ }
+
+ public boolean isPressingOnShifted() {
+ return mState == PRESSING_ON_SHIFTED;
+ }
+
+ public boolean isIgnoring() {
+ return mState == IGNORING;
+ }
+
+ @Override
+ public String toString() {
+ return toString(mState);
+ }
+
+ @Override
+ protected String toString(int state) {
+ switch (state) {
+ case PRESSING_ON_SHIFTED: return "PRESSING_ON_SHIFTED";
+ case IGNORING: return "IGNORING";
+ default: return super.toString(state);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/SlidingLocaleDrawable.java b/java/src/com/android/inputmethod/keyboard/internal/SlidingLocaleDrawable.java
new file mode 100644
index 000000000..f8c81adfb
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/SlidingLocaleDrawable.java
@@ -0,0 +1,161 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.text.TextPaint;
+import android.view.ViewConfiguration;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.LatinKeyboard;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+
+/**
+ * Animation to be displayed on the spacebar preview popup when switching languages by swiping the
+ * spacebar. It draws the current, previous and next languages and moves them by the delta of touch
+ * movement on the spacebar.
+ */
+public class SlidingLocaleDrawable extends Drawable {
+ private static final int SLIDE_SPEED_MULTIPLIER_RATIO = 150;
+ private final int mWidth;
+ private final int mHeight;
+ private final Drawable mBackground;
+ private final int mSpacebarTextColor;
+ private final TextPaint mTextPaint;
+ private final int mMiddleX;
+ private final Drawable mLeftDrawable;
+ private final Drawable mRightDrawable;
+ private final int mThreshold;
+
+ private int mDiff;
+ private boolean mHitThreshold;
+ private String mCurrentLanguage;
+ private String mNextLanguage;
+ private String mPrevLanguage;
+
+ public SlidingLocaleDrawable(Context context, Drawable background, int width, int height) {
+ mBackground = background;
+ Keyboard.setDefaultBounds(background);
+ mWidth = width;
+ mHeight = height;
+ final TextPaint textPaint = new TextPaint();
+ textPaint.setTextSize(LatinKeyboard.getTextSizeFromTheme(
+ context.getTheme(), android.R.style.TextAppearance_Medium, 18));
+ textPaint.setColor(Color.TRANSPARENT);
+ textPaint.setTextAlign(Align.CENTER);
+ textPaint.setAntiAlias(true);
+ mTextPaint = textPaint;
+ mMiddleX = (background != null) ? (mWidth - mBackground.getIntrinsicWidth()) / 2 : 0;
+
+ final TypedArray a = context.obtainStyledAttributes(
+ null, R.styleable.LatinKeyboard, R.attr.latinKeyboardStyle, R.style.LatinKeyboard);
+ mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboard_spacebarTextColor, 0);
+ mLeftDrawable = a.getDrawable(R.styleable.LatinKeyboard_spacebarArrowPreviewLeftIcon);
+ mRightDrawable = a.getDrawable(R.styleable.LatinKeyboard_spacebarArrowPreviewRightIcon);
+ a.recycle();
+
+ mThreshold = ViewConfiguration.get(context).getScaledTouchSlop();
+ }
+
+ public void setDiff(int diff) {
+ if (diff == Integer.MAX_VALUE) {
+ mHitThreshold = false;
+ mCurrentLanguage = null;
+ return;
+ }
+ mDiff = Math.max(diff, diff * SLIDE_SPEED_MULTIPLIER_RATIO / 100);
+ if (mDiff > mWidth) mDiff = mWidth;
+ if (mDiff < -mWidth) mDiff = -mWidth;
+ if (Math.abs(mDiff) > mThreshold) mHitThreshold = true;
+ invalidateSelf();
+ }
+
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.save();
+ if (mHitThreshold) {
+ Paint paint = mTextPaint;
+ final int width = mWidth;
+ final int height = mHeight;
+ final int diff = mDiff;
+ final Drawable lArrow = mLeftDrawable;
+ final Drawable rArrow = mRightDrawable;
+ canvas.clipRect(0, 0, width, height);
+ if (mCurrentLanguage == null) {
+ SubtypeSwitcher subtypeSwitcher = SubtypeSwitcher.getInstance();
+ mCurrentLanguage = subtypeSwitcher.getInputLanguageName();
+ mNextLanguage = subtypeSwitcher.getNextInputLanguageName();
+ mPrevLanguage = subtypeSwitcher.getPreviousInputLanguageName();
+ }
+ // Draw language text with shadow
+ final float baseline = mHeight * LatinKeyboard.SPACEBAR_LANGUAGE_BASELINE
+ - paint.descent();
+ paint.setColor(mSpacebarTextColor);
+ canvas.drawText(mCurrentLanguage, width / 2 + diff, baseline, paint);
+ canvas.drawText(mNextLanguage, diff - width / 2, baseline, paint);
+ canvas.drawText(mPrevLanguage, diff + width + width / 2, baseline, paint);
+
+ if (lArrow != null && rArrow != null) {
+ Keyboard.setDefaultBounds(lArrow);
+ rArrow.setBounds(width - rArrow.getIntrinsicWidth(), 0, width,
+ rArrow.getIntrinsicHeight());
+ lArrow.draw(canvas);
+ rArrow.draw(canvas);
+ }
+ }
+ if (mBackground != null) {
+ canvas.translate(mMiddleX, 0);
+ mBackground.draw(canvas);
+ }
+ canvas.restore();
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // Ignore
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ // Ignore
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mHeight;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/SwipeTracker.java b/java/src/com/android/inputmethod/keyboard/internal/SwipeTracker.java
index 970e91965..8d192c2f0 100644
--- a/java/src/com/android/inputmethod/latin/SwipeTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/SwipeTracker.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 Google Inc.
+ * 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
@@ -14,11 +14,11 @@
* the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.keyboard.internal;
import android.view.MotionEvent;
-class SwipeTracker {
+public class SwipeTracker {
private static final int NUM_PAST = 4;
private static final int LONGEST_PAST_TIME = 200;
@@ -91,7 +91,7 @@ class SwipeTracker {
return mYVelocity;
}
- static class EventRingBuffer {
+ public static class EventRingBuffer {
private final int bufSize;
private final float xBuf[];
private final float yBuf[];
@@ -154,4 +154,4 @@ class SwipeTracker {
end = advance(end);
}
}
-} \ No newline at end of file
+}
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
new file mode 100644
index 000000000..074ecacc5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
@@ -0,0 +1,52 @@
+/*
+ * 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.latin;
+
+import java.io.File;
+
+/**
+ * Immutable class to hold the address of an asset.
+ * As opposed to a normal file, an asset is usually represented as a contiguous byte array in
+ * the package file. Open it correctly thus requires the name of the package it is in, but
+ * also the offset in the file and the length of this data. This class encapsulates these three.
+ */
+class AssetFileAddress {
+ public final String mFilename;
+ public final long mOffset;
+ public final long mLength;
+
+ public AssetFileAddress(final String filename, final long offset, final long length) {
+ mFilename = filename;
+ mOffset = offset;
+ mLength = length;
+ }
+
+ public static AssetFileAddress makeFromFileName(final String filename) {
+ if (null == filename) return null;
+ File f = new File(filename);
+ if (null == f || !f.isFile()) return null;
+ return new AssetFileAddress(filename, 0l, f.length());
+ }
+
+ public static AssetFileAddress makeFromFileNameAndOffset(final String filename,
+ final long offset, final long length) {
+ if (null == filename) return null;
+ File f = new File(filename);
+ if (null == f || !f.isFile()) return null;
+ return new AssetFileAddress(filename, offset, length);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
new file mode 100644
index 000000000..d3119792c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -0,0 +1,143 @@
+/*
+ * 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.latin;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+public class AutoCorrection {
+ private static final boolean DBG = LatinImeLogger.sDBG;
+ private static final String TAG = AutoCorrection.class.getSimpleName();
+ private boolean mHasAutoCorrection;
+ private CharSequence mAutoCorrectionWord;
+ private double mNormalizedScore;
+
+ public void init() {
+ mHasAutoCorrection = false;
+ mAutoCorrectionWord = null;
+ mNormalizedScore = Integer.MIN_VALUE;
+ }
+
+ public boolean hasAutoCorrection() {
+ return mHasAutoCorrection;
+ }
+
+ public CharSequence getAutoCorrectionWord() {
+ return mAutoCorrectionWord;
+ }
+
+ public double getNormalizedScore() {
+ return mNormalizedScore;
+ }
+
+ public void updateAutoCorrectionStatus(Map<String, Dictionary> dictionaries,
+ WordComposer wordComposer, ArrayList<CharSequence> suggestions, int[] sortedScores,
+ CharSequence typedWord, double autoCorrectionThreshold, int correctionMode,
+ CharSequence quickFixedWord, CharSequence whitelistedWord) {
+ if (hasAutoCorrectionForWhitelistedWord(whitelistedWord)) {
+ mHasAutoCorrection = true;
+ mAutoCorrectionWord = whitelistedWord;
+ } else if (hasAutoCorrectionForTypedWord(
+ dictionaries, wordComposer, suggestions, typedWord, correctionMode)) {
+ mHasAutoCorrection = true;
+ mAutoCorrectionWord = typedWord;
+ } else if (hasAutoCorrectionForQuickFix(quickFixedWord)) {
+ mHasAutoCorrection = true;
+ mAutoCorrectionWord = quickFixedWord;
+ } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions, correctionMode,
+ sortedScores, typedWord, autoCorrectionThreshold)) {
+ mHasAutoCorrection = true;
+ mAutoCorrectionWord = suggestions.get(0);
+ }
+ }
+
+ public static boolean isValidWord(
+ Map<String, Dictionary> dictionaries, CharSequence word, boolean ignoreCase) {
+ if (TextUtils.isEmpty(word)) {
+ return false;
+ }
+ final CharSequence lowerCasedWord = word.toString().toLowerCase();
+ for (final String key : dictionaries.keySet()) {
+ if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
+ final Dictionary dictionary = dictionaries.get(key);
+ if (dictionary.isValidWord(word)
+ || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isValidWordForAutoCorrection(
+ Map<String, Dictionary> dictionaries, CharSequence word, boolean ignoreCase) {
+ final Dictionary whiteList = dictionaries.get(Suggest.DICT_KEY_WHITELIST);
+ // If "word" is in the whitelist dictionary, it should not be auto corrected.
+ if (whiteList != null && whiteList.isValidWord(word)) {
+ return false;
+ }
+ return isValidWord(dictionaries, word, ignoreCase);
+ }
+
+ private static boolean hasAutoCorrectionForWhitelistedWord(CharSequence whiteListedWord) {
+ return whiteListedWord != null;
+ }
+
+ private boolean hasAutoCorrectionForTypedWord(Map<String, Dictionary> dictionaries,
+ WordComposer wordComposer, ArrayList<CharSequence> suggestions, CharSequence typedWord,
+ int correctionMode) {
+ if (TextUtils.isEmpty(typedWord)) return false;
+ boolean isValidWord = isValidWordForAutoCorrection(dictionaries, typedWord, false);
+ return wordComposer.size() > 1 && suggestions.size() > 0 && isValidWord
+ && (correctionMode == Suggest.CORRECTION_FULL
+ || correctionMode == Suggest.CORRECTION_FULL_BIGRAM);
+ }
+
+ private static boolean hasAutoCorrectionForQuickFix(CharSequence quickFixedWord) {
+ return quickFixedWord != null;
+ }
+
+ private boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer,
+ ArrayList<CharSequence> suggestions, int correctionMode, int[] sortedScores,
+ CharSequence typedWord, double autoCorrectionThreshold) {
+ if (wordComposer.size() > 1 && (correctionMode == Suggest.CORRECTION_FULL
+ || correctionMode == Suggest.CORRECTION_FULL_BIGRAM)
+ && typedWord != null && suggestions.size() > 0 && sortedScores.length > 0) {
+ final CharSequence autoCorrectionCandidate = suggestions.get(0);
+ final int autoCorrectionCandidateScore = sortedScores[0];
+ // TODO: when the normalized score of the first suggestion is nearly equals to
+ // the normalized score of the second suggestion, behave less aggressive.
+ mNormalizedScore = Utils.calcNormalizedScore(
+ typedWord,autoCorrectionCandidate, autoCorrectionCandidateScore);
+ if (DBG) {
+ Log.d(TAG, "Normalized " + typedWord + "," + autoCorrectionCandidate + ","
+ + autoCorrectionCandidateScore + ", " + mNormalizedScore
+ + "(" + autoCorrectionThreshold + ")");
+ }
+ if (mNormalizedScore >= autoCorrectionThreshold) {
+ if (DBG) {
+ Log.d(TAG, "Auto corrected by S-threshold.");
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java
index 4fbb5b012..460930f16 100644
--- a/java/src/com/android/inputmethod/latin/AutoDictionary.java
+++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 Google Inc.
+ * 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
@@ -16,10 +16,6 @@
package com.android.inputmethod.latin;
-import java.util.HashMap;
-import java.util.Set;
-import java.util.Map.Entry;
-
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
@@ -30,6 +26,10 @@ import android.os.AsyncTask;
import android.provider.BaseColumns;
import android.util.Log;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.Set;
+
/**
* Stores new words temporarily until they are promoted to the user dictionary
* for longevity. Words in the auto dictionary are used to determine if it's ok
@@ -41,13 +41,8 @@ public class AutoDictionary extends ExpandableDictionary {
static final int FREQUENCY_FOR_PICKED = 3;
// Weight added to a user typing a new word that doesn't get corrected (or is reverted)
static final int FREQUENCY_FOR_TYPED = 1;
- // A word that is frequently typed and gets promoted to the user dictionary, uses this
- // frequency.
- static final int FREQUENCY_FOR_AUTO_ADD = 250;
// If the user touches a typed word 2 times or more, it will become valid.
private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED;
- // If the user touches a typed word 4 times or more, it will be added to the user dict.
- private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED;
private LatinIME mIme;
// Locale for which this auto dictionary is storing words
@@ -98,7 +93,7 @@ public class AutoDictionary extends ExpandableDictionary {
}
@Override
- public boolean isValidWord(CharSequence word) {
+ public synchronized boolean isValidWord(CharSequence word) {
final int frequency = getWordFrequency(word);
return frequency >= VALIDITY_THRESHOLD;
}
@@ -138,7 +133,8 @@ public class AutoDictionary extends ExpandableDictionary {
}
@Override
- public void addWord(String word, int addFrequency) {
+ public void addWord(String newWord, int addFrequency) {
+ String word = newWord;
final int length = word.length();
// Don't add very short or very long words.
if (length < 2 || length > getMaxWordLength()) return;
@@ -150,11 +146,6 @@ public class AutoDictionary extends ExpandableDictionary {
freq = freq < 0 ? addFrequency : freq + addFrequency;
super.addWord(word, freq);
- if (freq >= PROMOTION_THRESHOLD) {
- mIme.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD);
- freq = 0;
- }
-
synchronized (mPendingWritesLock) {
// Write a null frequency if it is to be deleted from the db
mPendingWrites.put(word, freq == 0 ? null : new Integer(freq));
@@ -224,7 +215,7 @@ public class AutoDictionary extends ExpandableDictionary {
private final DatabaseHelper mDbHelper;
private final String mLocale;
- public UpdateDbTask(Context context, DatabaseHelper openHelper,
+ public UpdateDbTask(@SuppressWarnings("unused") Context context, DatabaseHelper openHelper,
HashMap<String, Integer> pendingWrites, String locale) {
mMap = pendingWrites;
mLocale = locale;
diff --git a/java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java b/java/src/com/android/inputmethod/latin/BackupAgent.java
index a14a4751f..ee070af75 100644
--- a/java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java
+++ b/java/src/com/android/inputmethod/latin/BackupAgent.java
@@ -22,7 +22,7 @@ import android.app.backup.SharedPreferencesBackupHelper;
/**
* Backs up the Latin IME shared preferences.
*/
-public class LatinIMEBackupAgent extends BackupAgentHelper {
+public class BackupAgent extends BackupAgentHelper {
@Override
public void onCreate() {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index d0e143dd0..9748d6006 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -16,224 +16,193 @@
package com.android.inputmethod.latin;
-import java.io.InputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.channels.Channels;
-import java.util.Arrays;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.ProximityInfo;
import android.content.Context;
-import android.util.Log;
+
+import java.util.Arrays;
/**
* Implements a static, compacted, binary dictionary of standard words.
*/
public class BinaryDictionary extends Dictionary {
+ public static final String DICTIONARY_PACK_AUTHORITY =
+ "com.android.inputmethod.latin.dictionarypack";
+
/**
- * There is difference between what java and native code can handle.
+ * There is a difference between what java and native code can handle.
* This value should only be used in BinaryDictionary.java
* It is necessary to keep it at this value because some languages e.g. German have
* really long words.
*/
- protected static final int MAX_WORD_LENGTH = 48;
+ public static final int MAX_WORD_LENGTH = 48;
+ public static final int MAX_WORDS = 18;
+ @SuppressWarnings("unused")
private static final String TAG = "BinaryDictionary";
- private static final int MAX_ALTERNATIVES = 16;
- private static final int MAX_WORDS = 18;
+ private static final int MAX_PROXIMITY_CHARS_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE;
private static final int MAX_BIGRAMS = 60;
private static final int TYPED_LETTER_MULTIPLIER = 2;
- private static final boolean ENABLE_MISSED_CHARACTERS = true;
private int mDicTypeId;
private int mNativeDict;
- private int mDictLength;
- private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES];
- private char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
- private char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS];
- private int[] mFrequencies = new int[MAX_WORDS];
- private int[] mFrequencies_bigrams = new int[MAX_BIGRAMS];
- // Keep a reference to the native dict direct buffer in Java to avoid
- // unexpected deallocation of the direct buffer.
- private ByteBuffer mNativeDictDirectBuffer;
+ private final int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_PROXIMITY_CHARS_SIZE];
+ private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
+ private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS];
+ private final int[] mScores = new int[MAX_WORDS];
+ private final int[] mBigramScores = new int[MAX_BIGRAMS];
- static {
- try {
- System.loadLibrary("jni_latinime");
- } catch (UnsatisfiedLinkError ule) {
- Log.e("BinaryDictionary", "Could not load native library jni_latinime");
- }
- }
+ private final KeyboardSwitcher mKeyboardSwitcher = KeyboardSwitcher.getInstance();
+
+ public static final Flag FLAG_REQUIRES_GERMAN_UMLAUT_PROCESSING =
+ new Flag(R.bool.config_require_umlaut_processing, 0x1);
+
+ // Can create a new flag from extravalue :
+ // public static final Flag FLAG_MYFLAG =
+ // new Flag("my_flag", 0x02);
+
+ private static final Flag[] ALL_FLAGS = {
+ // Here should reside all flags that trigger some special processing
+ // These *must* match the definition in UnigramDictionary enum in
+ // unigram_dictionary.h so please update both at the same time.
+ FLAG_REQUIRES_GERMAN_UMLAUT_PROCESSING,
+ };
+
+ private int mFlags = 0;
/**
- * Create a dictionary from a raw resource file
- * @param context application context for reading resources
- * @param resId the resource containing the raw binary dictionary
+ * Constructor for the binary dictionary. This is supposed to be called from the
+ * dictionary factory.
+ * All implementations should pass null into flagArray, except for testing purposes.
+ * @param context the context to access the environment from.
+ * @param filename the name of the file to read through native code.
+ * @param offset the offset of the dictionary data within the file.
+ * @param length the length of the binary data.
+ * @param flagArray the flags to limit the dictionary to, or null for default.
*/
- public BinaryDictionary(Context context, int[] resId, int dicTypeId) {
- if (resId != null && resId.length > 0 && resId[0] != 0) {
- loadDictionary(context, resId);
- }
- mDicTypeId = dicTypeId;
+ public BinaryDictionary(final Context context,
+ final String filename, final long offset, final long length, Flag[] flagArray) {
+ // Note: at the moment a binary dictionary is always of the "main" type.
+ // Initializing this here will help transitioning out of the scheme where
+ // the Suggest class knows everything about every single dictionary.
+ mDicTypeId = Suggest.DIC_MAIN;
+ // TODO: Stop relying on the state of SubtypeSwitcher, get it as a parameter
+ mFlags = Flag.initFlags(null == flagArray ? ALL_FLAGS : flagArray, context,
+ SubtypeSwitcher.getInstance());
+ loadDictionary(filename, offset, length);
}
- /**
- * Create a dictionary from a byte buffer. This is used for testing.
- * @param context application context for reading resources
- * @param byteBuffer a ByteBuffer containing the binary dictionary
- */
- public BinaryDictionary(Context context, ByteBuffer byteBuffer, int dicTypeId) {
- if (byteBuffer != null) {
- if (byteBuffer.isDirect()) {
- mNativeDictDirectBuffer = byteBuffer;
- } else {
- mNativeDictDirectBuffer = ByteBuffer.allocateDirect(byteBuffer.capacity());
- byteBuffer.rewind();
- mNativeDictDirectBuffer.put(byteBuffer);
- }
- mDictLength = byteBuffer.capacity();
- mNativeDict = openNative(mNativeDictDirectBuffer,
- TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER);
- }
- mDicTypeId = dicTypeId;
+ static {
+ Utils.loadNativeLibrary();
}
- private native int openNative(ByteBuffer bb, int typedLetterMultiplier,
- int fullWordMultiplier);
+ private native int openNative(String sourceDir, long dictOffset, long dictSize,
+ int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength,
+ int maxWords, int maxAlternatives);
private native void closeNative(int dict);
private native boolean isValidWordNative(int nativeData, char[] word, int wordLength);
- private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize,
- char[] outputChars, int[] frequencies, int maxWordLength, int maxWords,
- int maxAlternatives, int skipPos, int[] nextLettersFrequencies, int nextLettersSize);
+ private native int getSuggestionsNative(int dict, int proximityInfo, int[] xCoordinates,
+ int[] yCoordinates, int[] inputCodes, int codesSize, int flags, char[] outputChars,
+ int[] scores);
private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength,
- int[] inputCodes, int inputCodesLength, char[] outputChars, int[] frequencies,
+ int[] inputCodes, int inputCodesLength, char[] outputChars, int[] scores,
int maxWordLength, int maxBigrams, int maxAlternatives);
- private final void loadDictionary(Context context, int[] resId) {
- InputStream[] is = null;
- try {
- // merging separated dictionary into one if dictionary is separated
- int total = 0;
- is = new InputStream[resId.length];
- for (int i = 0; i < resId.length; i++) {
- is[i] = context.getResources().openRawResource(resId[i]);
- total += is[i].available();
- }
-
- mNativeDictDirectBuffer =
- ByteBuffer.allocateDirect(total).order(ByteOrder.nativeOrder());
- int got = 0;
- for (int i = 0; i < resId.length; i++) {
- got += Channels.newChannel(is[i]).read(mNativeDictDirectBuffer);
- }
- if (got != total) {
- Log.e(TAG, "Read " + got + " bytes, expected " + total);
- } else {
- mNativeDict = openNative(mNativeDictDirectBuffer,
- TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER);
- mDictLength = total;
- }
- } catch (IOException e) {
- Log.w(TAG, "No available memory for binary dictionary");
- } finally {
- try {
- if (is != null) {
- for (int i = 0; i < is.length; i++) {
- is[i].close();
- }
- }
- } catch (IOException e) {
- Log.w(TAG, "Failed to close input stream");
- }
- }
+ private final void loadDictionary(String path, long startOffset, long length) {
+ mNativeDict = openNative(path, startOffset, length,
+ TYPED_LETTER_MULTIPLIER, FULL_WORD_SCORE_MULTIPLIER,
+ MAX_WORD_LENGTH, MAX_WORDS, MAX_PROXIMITY_CHARS_SIZE);
}
-
@Override
public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback, int[] nextLettersFrequencies) {
+ final WordCallback callback) {
+ if (mNativeDict == 0) return;
char[] chars = previousWord.toString().toCharArray();
Arrays.fill(mOutputChars_bigrams, (char) 0);
- Arrays.fill(mFrequencies_bigrams, 0);
+ Arrays.fill(mBigramScores, 0);
int codesSize = codes.size();
+ if (codesSize <= 0) {
+ // Do not return bigrams from BinaryDictionary when nothing was typed.
+ // Only use user-history bigrams (or whatever other bigram dictionaries decide).
+ return;
+ }
Arrays.fill(mInputCodes, -1);
int[] alternatives = codes.getCodesAt(0);
System.arraycopy(alternatives, 0, mInputCodes, 0,
- Math.min(alternatives.length, MAX_ALTERNATIVES));
+ Math.min(alternatives.length, MAX_PROXIMITY_CHARS_SIZE));
int count = getBigramsNative(mNativeDict, chars, chars.length, mInputCodes, codesSize,
- mOutputChars_bigrams, mFrequencies_bigrams, MAX_WORD_LENGTH, MAX_BIGRAMS,
- MAX_ALTERNATIVES);
+ mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS,
+ MAX_PROXIMITY_CHARS_SIZE);
- for (int j = 0; j < count; j++) {
- if (mFrequencies_bigrams[j] < 1) break;
- int start = j * MAX_WORD_LENGTH;
+ for (int j = 0; j < count; ++j) {
+ if (mBigramScores[j] < 1) break;
+ final int start = j * MAX_WORD_LENGTH;
int len = 0;
- while (mOutputChars_bigrams[start + len] != 0) {
- len++;
+ while (len < MAX_WORD_LENGTH && mOutputChars_bigrams[start + len] != 0) {
+ ++len;
}
if (len > 0) {
- callback.addWord(mOutputChars_bigrams, start, len, mFrequencies_bigrams[j],
+ callback.addWord(mOutputChars_bigrams, start, len, mBigramScores[j],
mDicTypeId, DataType.BIGRAM);
}
}
}
@Override
- public void getWords(final WordComposer codes, final WordCallback callback,
- int[] nextLettersFrequencies) {
- final int codesSize = codes.size();
- // Won't deal with really long words.
- if (codesSize > MAX_WORD_LENGTH - 1) return;
-
- Arrays.fill(mInputCodes, -1);
- for (int i = 0; i < codesSize; i++) {
- int[] alternatives = codes.getCodesAt(i);
- System.arraycopy(alternatives, 0, mInputCodes, i * MAX_ALTERNATIVES,
- Math.min(alternatives.length, MAX_ALTERNATIVES));
- }
- Arrays.fill(mOutputChars, (char) 0);
- Arrays.fill(mFrequencies, 0);
-
- int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize,
- mOutputChars, mFrequencies,
- MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1,
- nextLettersFrequencies,
- nextLettersFrequencies != null ? nextLettersFrequencies.length : 0);
-
- // If there aren't sufficient suggestions, search for words by allowing wild cards at
- // the different character positions. This feature is not ready for prime-time as we need
- // to figure out the best ranking for such words compared to proximity corrections and
- // completions.
- if (ENABLE_MISSED_CHARACTERS && count < 5) {
- for (int skip = 0; skip < codesSize; skip++) {
- int tempCount = getSuggestionsNative(mNativeDict, mInputCodes, codesSize,
- mOutputChars, mFrequencies,
- MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, skip,
- null, 0);
- count = Math.max(count, tempCount);
- if (tempCount > 0) break;
- }
- }
+ public void getWords(final WordComposer codes, final WordCallback callback) {
+ final int count = getSuggestions(codes, mKeyboardSwitcher.getLatinKeyboard(),
+ mOutputChars, mScores);
- for (int j = 0; j < count; j++) {
- if (mFrequencies[j] < 1) break;
- int start = j * MAX_WORD_LENGTH;
+ for (int j = 0; j < count; ++j) {
+ if (mScores[j] < 1) break;
+ final int start = j * MAX_WORD_LENGTH;
int len = 0;
- while (mOutputChars[start + len] != 0) {
- len++;
+ while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) {
+ ++len;
}
if (len > 0) {
- callback.addWord(mOutputChars, start, len, mFrequencies[j], mDicTypeId,
+ callback.addWord(mOutputChars, start, len, mScores[j], mDicTypeId,
DataType.UNIGRAM);
}
}
}
+ /* package for test */ boolean isValidDictionary() {
+ return mNativeDict != 0;
+ }
+
+ /* package for test */ int getSuggestions(final WordComposer codes, final Keyboard keyboard,
+ char[] outputChars, int[] scores) {
+ if (!isValidDictionary()) return -1;
+
+ final int codesSize = codes.size();
+ // Won't deal with really long words.
+ if (codesSize > MAX_WORD_LENGTH - 1) return -1;
+
+ Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE);
+ for (int i = 0; i < codesSize; i++) {
+ int[] alternatives = codes.getCodesAt(i);
+ System.arraycopy(alternatives, 0, mInputCodes, i * MAX_PROXIMITY_CHARS_SIZE,
+ Math.min(alternatives.length, MAX_PROXIMITY_CHARS_SIZE));
+ }
+ Arrays.fill(outputChars, (char) 0);
+ Arrays.fill(scores, 0);
+
+ final int proximityInfo = keyboard == null ? 0 : keyboard.getProximityInfo();
+ return getSuggestionsNative(
+ mNativeDict, proximityInfo,
+ codes.getXCoordinates(), codes.getYCoordinates(), mInputCodes, codesSize,
+ mFlags, outputChars, scores);
+ }
+
@Override
public boolean isValidWord(CharSequence word) {
if (word == null) return false;
@@ -241,12 +210,12 @@ public class BinaryDictionary extends Dictionary {
return isValidWordNative(mNativeDict, chars, chars.length);
}
- public int getSize() {
- return mDictLength; // This value is initialized on the call to openNative()
- }
-
@Override
public synchronized void close() {
+ closeInternal();
+ }
+
+ private void closeInternal() {
if (mNativeDict != 0) {
closeNative(mNativeDict);
mNativeDict = 0;
@@ -255,7 +224,10 @@ public class BinaryDictionary extends Dictionary {
@Override
protected void finalize() throws Throwable {
- close();
- super.finalize();
+ try {
+ closeInternal();
+ } finally {
+ super.finalize();
+ }
}
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
new file mode 100644
index 000000000..76a230f82
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -0,0 +1,151 @@
+/*
+ * 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.latin;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Group class for static methods to help with creation and getting of the binary dictionary
+ * file from the dictionary provider
+ */
+public class BinaryDictionaryFileDumper {
+ /**
+ * The size of the temporary buffer to copy files.
+ */
+ static final int FILE_READ_BUFFER_SIZE = 1024;
+
+ // Prevents this class to be accidentally instantiated.
+ private BinaryDictionaryFileDumper() {
+ }
+
+ /**
+ * Generates a file name that matches the locale passed as an argument.
+ * The file name is basically the result of the .toString() method, except we replace
+ * any @File.separator with an underscore to avoid generating a file name that may not
+ * be created.
+ * @param locale the locale for which to get the file name
+ * @param context the context to use for getting the directory
+ * @return the name of the file to be created
+ */
+ private static String getCacheFileNameForLocale(Locale locale, Context context) {
+ // The following assumes two things :
+ // 1. That File.separator is not the same character as "_"
+ // I don't think any android system will ever use "_" as a path separator
+ // 2. That no two locales differ by only a File.separator versus a "_"
+ // Since "_" can't be part of locale components this should be safe.
+ // Examples:
+ // en -> en
+ // en_US_POSIX -> en_US_POSIX
+ // en__foo/bar -> en__foo_bar
+ final String[] separator = { File.separator };
+ final String[] empty = { "_" };
+ final CharSequence basename = TextUtils.replace(locale.toString(), separator, empty);
+ return context.getFilesDir() + File.separator + basename;
+ }
+
+ /**
+ * Return for a given locale the provider URI to query to get the dictionary.
+ */
+ public static Uri getProviderUri(Locale locale) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(BinaryDictionary.DICTIONARY_PACK_AUTHORITY).appendPath(
+ locale.toString()).build();
+ }
+
+ /**
+ * Queries a content provider for dictionary data for some locale and returns the file addresses
+ *
+ * This will query a content provider for dictionary data for a given locale, and return
+ * the addresses of a file set the members of which are suitable to be mmap'ed. It will copy
+ * them to local storage if needed.
+ * It should also check the dictionary versions to avoid unnecessary copies but this is
+ * still in TODO state.
+ * This will make the data from the content provider the cached dictionary for this locale,
+ * overwriting any previous cached data.
+ * @returns the addresses of the files, or null if no data could be obtained.
+ * @throw FileNotFoundException if the provider returns non-existent data.
+ * @throw IOException if the provider-returned data could not be read.
+ */
+ public static List<AssetFileAddress> getDictSetFromContentProvider(Locale locale,
+ Context context) throws FileNotFoundException, IOException {
+ // TODO: check whether the dictionary is the same or not and if it is, return the cached
+ // file.
+ // TODO: This should be able to read a number of files from the dictionary pack, copy
+ // them all and return them.
+ final ContentResolver resolver = context.getContentResolver();
+ final Uri dictionaryPackUri = getProviderUri(locale);
+ final AssetFileDescriptor afd = resolver.openAssetFileDescriptor(dictionaryPackUri, "r");
+ if (null == afd) return null;
+ final String fileName =
+ copyFileTo(afd.createInputStream(), getCacheFileNameForLocale(locale, context));
+ return Arrays.asList(AssetFileAddress.makeFromFileName(fileName));
+ }
+
+ /**
+ * Accepts a file as dictionary data for some locale and returns the name of a file.
+ *
+ * This will make the data in the input file the cached dictionary for this locale, overwriting
+ * any previous cached data.
+ */
+ public static String getDictionaryFileFromFile(String fileName, Locale locale,
+ Context context) throws FileNotFoundException, IOException {
+ return copyFileTo(new FileInputStream(fileName), getCacheFileNameForLocale(locale,
+ context));
+ }
+
+ /**
+ * Accepts a resource number as dictionary data for some locale and returns the name of a file.
+ *
+ * This will make the resource the cached dictionary for this locale, overwriting any previous
+ * cached data.
+ */
+ public static String getDictionaryFileFromResource(int resource, Locale locale,
+ Context context) throws FileNotFoundException, IOException {
+ return copyFileTo(context.getResources().openRawResource(resource),
+ getCacheFileNameForLocale(locale, context));
+ }
+
+ /**
+ * Copies the data in an input stream to a target file, creating the file if necessary and
+ * overwriting it if it already exists.
+ * @param input the stream to be copied.
+ * @param outputFileName the name of a file to copy the data to. It is created if necessary.
+ */
+ private static String copyFileTo(final InputStream input, final String outputFileName)
+ throws FileNotFoundException, IOException {
+ final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE];
+ final FileOutputStream output = new FileOutputStream(outputFileName);
+ for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer))
+ output.write(buffer, 0, readBytes);
+ input.close();
+ return outputFileName;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
new file mode 100644
index 000000000..7ce92920d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -0,0 +1,97 @@
+/*
+ * 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.latin;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Helper class to get the address of a mmap'able dictionary file.
+ */
+class BinaryDictionaryGetter {
+
+ /**
+ * Used for Log actions from this class
+ */
+ private static final String TAG = BinaryDictionaryGetter.class.getSimpleName();
+
+ // Prevents this from being instantiated
+ private BinaryDictionaryGetter() {}
+
+ /**
+ * Returns a file address from a resource, or null if it cannot be opened.
+ */
+ private static AssetFileAddress loadFallbackResource(Context context, int fallbackResId) {
+ final AssetFileDescriptor afd = context.getResources().openRawResourceFd(fallbackResId);
+ if (afd == null) {
+ Log.e(TAG, "Found the resource but cannot read it. Is it compressed? resId="
+ + fallbackResId);
+ return null;
+ }
+ return AssetFileAddress.makeFromFileNameAndOffset(
+ context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
+ }
+
+ /**
+ * Returns a list of file addresses for a given locale, trying relevant methods in order.
+ *
+ * Tries to get binary dictionaries from various sources, in order:
+ * - Uses a private method of getting a private dictionaries, as implemented by the
+ * PrivateBinaryDictionaryGetter class.
+ * If that fails:
+ * - Uses a content provider to get a public dictionary set, as per the protocol described
+ * in BinaryDictionaryFileDumper.
+ * If that fails:
+ * - Gets a file name from the fallback resource passed as an argument.
+ * If that fails:
+ * - Returns null.
+ * @return The address of a valid file, or null.
+ */
+ public static List<AssetFileAddress> getDictionaryFiles(Locale locale, Context context,
+ int fallbackResId) {
+ // Try first to query a private package signed the same way for private files.
+ final List<AssetFileAddress> privateFiles =
+ PrivateBinaryDictionaryGetter.getDictionaryFiles(locale, context);
+ if (null != privateFiles) {
+ return privateFiles;
+ } else {
+ try {
+ // If that was no-go, try to find a publicly exported dictionary.
+ List<AssetFileAddress> listFromContentProvider =
+ BinaryDictionaryFileDumper.getDictSetFromContentProvider(locale, context);
+ if (null != listFromContentProvider) {
+ return listFromContentProvider;
+ }
+ // If the list is null, fall through and return the fallback
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Unable to create dictionary file from provider for locale "
+ + locale.toString() + ": falling back to internal dictionary");
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to read source data for locale "
+ + locale.toString() + ": falling back to internal dictionary");
+ }
+ return Arrays.asList(loadFallbackResource(context, fallbackResId));
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java
index 68f288925..09fd3b473 100755..100644
--- a/java/src/com/android/inputmethod/latin/CandidateView.java
+++ b/java/src/com/android/inputmethod/latin/CandidateView.java
@@ -1,12 +1,12 @@
/*
- * Copyright (C) 2008 The Android Open Source Project
- *
+ * 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
@@ -18,75 +18,128 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.Rect;
+import android.content.res.TypedArray;
+import android.graphics.Color;
import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
-import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewGroup.LayoutParams;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
+import com.android.inputmethod.compat.FrameLayoutCompatUtils;
+import com.android.inputmethod.compat.LinearLayoutCompatUtils;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
-public class CandidateView extends View {
-
- private static final int OUT_OF_BOUNDS_WORD_INDEX = -1;
- private static final int OUT_OF_BOUNDS_X_COORD = -1;
+public class CandidateView extends LinearLayout implements OnClickListener, OnLongClickListener {
- private LatinIME mService;
- private final ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
- private boolean mShowingCompletions;
- private CharSequence mSelectedString;
- private int mSelectedIndex;
- private int mTouchX = OUT_OF_BOUNDS_X_COORD;
- private final Drawable mSelectionHighlight;
- private boolean mTypedWordValid;
-
- private boolean mHaveMinimalSuggestion;
-
- private Rect mBgPadding;
+ public interface Listener {
+ public boolean addWordToDictionary(String word);
+ public void pickSuggestionManually(int index, CharSequence word);
+ }
- private final TextView mPreviewText;
+ private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
+ private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
+ // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
+ private static final int MAX_SUGGESTIONS = 18;
+ private static final int UNSPECIFIED_MEASURESPEC = MeasureSpec.makeMeasureSpec(
+ 0, MeasureSpec.UNSPECIFIED);
+
+ private static final boolean DBG = LatinImeLogger.sDBG;
+
+ private static final int NUM_CANDIDATES_IN_STRIP = 3;
+ private final ImageView mExpandCandidatesPane;
+ private final ImageView mCloseCandidatesPane;
+ private ViewGroup mCandidatesPane;
+ private ViewGroup mCandidatesPaneContainer;
+ private View mKeyboardView;
+ private final ArrayList<TextView> mWords = new ArrayList<TextView>();
+ private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
+ private final ArrayList<View> mDividers = new ArrayList<View>();
+ private final int mCandidatePadding;
+ private final int mCandidateStripHeight;
+ private final CharacterStyle mInvertedForegroundColorSpan;
+ private final CharacterStyle mInvertedBackgroundColorSpan;
+ private final int mAutoCorrectHighlight;
+ private static final int AUTO_CORRECT_BOLD = 0x01;
+ private static final int AUTO_CORRECT_UNDERLINE = 0x02;
+ private static final int AUTO_CORRECT_INVERT = 0x04;
+ private final int mColorTypedWord;
+ private final int mColorAutoCorrect;
+ private final int mColorSuggestedCandidate;
private final PopupWindow mPreviewPopup;
- private int mCurrentWordIndex;
- private Drawable mDivider;
-
- private static final int MAX_SUGGESTIONS = 32;
- private static final int SCROLL_PIXELS = 20;
-
- private final int[] mWordWidth = new int[MAX_SUGGESTIONS];
- private final int[] mWordX = new int[MAX_SUGGESTIONS];
- private int mPopupPreviewX;
- private int mPopupPreviewY;
-
- private static final int X_GAP = 10;
-
- private final int mColorNormal;
- private final int mColorRecommended;
- private final int mColorOther;
- private final Paint mPaint;
- private final int mDescent;
- private boolean mScrolled;
+ private final TextView mPreviewText;
+
+ private Listener mListener;
+ private SuggestedWords mSuggestions = SuggestedWords.EMPTY;
+ private boolean mShowingAutoCorrectionInverted;
private boolean mShowingAddToDictionary;
- private CharSequence mAddToDictionaryHint;
- private int mTargetScrollX;
+ private final UiHandler mHandler = new UiHandler();
- private final int mMinTouchableWidth;
+ private class UiHandler extends Handler {
+ private static final int MSG_HIDE_PREVIEW = 0;
+ private static final int MSG_UPDATE_SUGGESTION = 1;
- private int mTotalWidth;
-
- private final GestureDetector mGestureDetector;
+ private static final long DELAY_HIDE_PREVIEW = 1000;
+ private static final long DELAY_UPDATE_SUGGESTION = 300;
+
+ @Override
+ public void dispatchMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_HIDE_PREVIEW:
+ hidePreview();
+ break;
+ case MSG_UPDATE_SUGGESTION:
+ updateSuggestions();
+ break;
+ }
+ }
+
+ public void postHidePreview() {
+ cancelHidePreview();
+ sendMessageDelayed(obtainMessage(MSG_HIDE_PREVIEW), DELAY_HIDE_PREVIEW);
+ }
+
+ public void cancelHidePreview() {
+ removeMessages(MSG_HIDE_PREVIEW);
+ }
+
+ public void postUpdateSuggestions() {
+ cancelUpdateSuggestions();
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION),
+ DELAY_UPDATE_SUGGESTION);
+ }
+
+ public void cancelUpdateSuggestions() {
+ removeMessages(MSG_UPDATE_SUGGESTION);
+ }
+
+ public void cancelAllMessages() {
+ cancelHidePreview();
+ cancelUpdateSuggestions();
+ }
+ }
/**
* Construct a CandidateView for showing suggested words for completion.
@@ -94,245 +147,298 @@ public class CandidateView extends View {
* @param attrs
*/
public CandidateView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.candidateViewStyle);
+ }
+
+ public CandidateView(Context context, AttributeSet attrs, int defStyle) {
+ // Note: Up to version 10 (Gingerbread) of the API, LinearLayout doesn't have 3-argument
+ // constructor.
+ // TODO: Call 3-argument constructor, super(context, attrs, defStyle), when we abandon
+ // backward compatibility with the version 10 or earlier of the API.
super(context, attrs);
- mSelectionHighlight = context.getResources().getDrawable(
- R.drawable.list_selector_background_pressed);
+ if (defStyle != R.attr.candidateViewStyle) {
+ throw new IllegalArgumentException(
+ "can't accept defStyle other than R.attr.candidayeViewStyle: defStyle="
+ + defStyle);
+ }
+ setBackgroundDrawable(LinearLayoutCompatUtils.getBackgroundDrawable(
+ context, attrs, defStyle, R.style.CandidateViewStyle));
- LayoutInflater inflate =
- (LayoutInflater) context
- .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Resources res = context.getResources();
+ LayoutInflater inflater = LayoutInflater.from(context);
+ inflater.inflate(R.layout.candidates_strip, this);
+
mPreviewPopup = new PopupWindow(context);
- mPreviewText = (TextView) inflate.inflate(R.layout.candidate_preview, null);
- mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ mPreviewText = (TextView) inflater.inflate(R.layout.candidate_preview, null);
+ mPreviewPopup.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
mPreviewPopup.setContentView(mPreviewText);
mPreviewPopup.setBackgroundDrawable(null);
- mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation);
- mColorNormal = res.getColor(R.color.candidate_normal);
- mColorRecommended = res.getColor(R.color.candidate_recommended);
- mColorOther = res.getColor(R.color.candidate_other);
- mDivider = res.getDrawable(R.drawable.keyboard_suggest_strip_divider);
- mAddToDictionaryHint = res.getString(R.string.hint_add_to_dictionary);
-
- mPaint = new Paint();
- mPaint.setColor(mColorNormal);
- mPaint.setAntiAlias(true);
- mPaint.setTextSize(mPreviewText.getTextSize());
- mPaint.setStrokeWidth(0);
- mPaint.setTextAlign(Align.CENTER);
- mDescent = (int) mPaint.descent();
- mMinTouchableWidth = (int)res.getDimension(R.dimen.candidate_min_touchable_width);
-
- mGestureDetector = new GestureDetector(
- new CandidateStripGestureListener(mMinTouchableWidth));
- setWillNotDraw(false);
- setHorizontalScrollBarEnabled(false);
- setVerticalScrollBarEnabled(false);
- scrollTo(0, getScrollY());
- }
-
- private class CandidateStripGestureListener extends GestureDetector.SimpleOnGestureListener {
- private final int mTouchSlopSquare;
- public CandidateStripGestureListener(int touchSlop) {
- // Slightly reluctant to scroll to be able to easily choose the suggestion
- mTouchSlopSquare = touchSlop * touchSlop;
- }
-
- @Override
- public void onLongPress(MotionEvent me) {
- if (mSuggestions.size() > 0) {
- if (me.getX() + getScrollX() < mWordWidth[0] && getScrollX() < 10) {
- longPressFirstWord();
- }
+ mCandidatePadding = res.getDimensionPixelOffset(R.dimen.candidate_padding);
+ mCandidateStripHeight = res.getDimensionPixelOffset(R.dimen.candidate_strip_height);
+ for (int i = 0; i < MAX_SUGGESTIONS; i++) {
+ final TextView word, info;
+ switch (i) {
+ case 0:
+ word = (TextView)findViewById(R.id.word_left);
+ word.setPadding(mCandidatePadding, 0, 0, 0);
+ info = (TextView)findViewById(R.id.info_left);
+ break;
+ case 1:
+ word = (TextView)findViewById(R.id.word_center);
+ info = (TextView)findViewById(R.id.info_center);
+ break;
+ case 2:
+ word = (TextView)findViewById(R.id.word_right);
+ info = (TextView)findViewById(R.id.info_right);
+ break;
+ default:
+ word = (TextView)inflater.inflate(R.layout.candidate_word, null);
+ info = (TextView)inflater.inflate(R.layout.candidate_info, null);
+ break;
}
- }
-
- @Override
- public boolean onDown(MotionEvent e) {
- mScrolled = false;
- return false;
- }
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2,
- float distanceX, float distanceY) {
- if (!mScrolled) {
- // This is applied only when we recognize that scrolling is starting.
- final int deltaX = (int) (e2.getX() - e1.getX());
- final int deltaY = (int) (e2.getY() - e1.getY());
- final int distance = (deltaX * deltaX) + (deltaY * deltaY);
- if (distance < mTouchSlopSquare) {
- return true;
- }
- mScrolled = true;
+ word.setTag(i);
+ word.setOnClickListener(this);
+ if (i == 0)
+ word.setOnLongClickListener(this);
+ mWords.add(word);
+ mInfos.add(info);
+ if (i > 0) {
+ final View divider = inflater.inflate(R.layout.candidate_divider, null);
+ divider.measure(UNSPECIFIED_MEASURESPEC, UNSPECIFIED_MEASURESPEC);
+ mDividers.add(divider);
}
+ }
- final int width = getWidth();
- mScrolled = true;
- int scrollX = getScrollX();
- scrollX += (int) distanceX;
- if (scrollX < 0) {
- scrollX = 0;
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.CandidateView, defStyle, R.style.CandidateViewStyle);
+ mAutoCorrectHighlight = a.getInt(R.styleable.CandidateView_autoCorrectHighlight, 0);
+ mColorTypedWord = a.getColor(R.styleable.CandidateView_colorTypedWord, 0);
+ mColorAutoCorrect = a.getColor(R.styleable.CandidateView_colorAutoCorrect, 0);
+ mColorSuggestedCandidate = a.getColor(R.styleable.CandidateView_colorSuggested, 0);
+ mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorTypedWord ^ 0x00ffffff);
+ mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord);
+
+ mExpandCandidatesPane = (ImageView)findViewById(R.id.expand_candidates_pane);
+ mExpandCandidatesPane.setImageDrawable(
+ a.getDrawable(R.styleable.CandidateView_iconExpandPane));
+ mExpandCandidatesPane.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ expandCandidatesPane();
}
- if (distanceX > 0 && scrollX + width > mTotalWidth) {
- scrollX -= (int) distanceX;
+ });
+ mCloseCandidatesPane = (ImageView)findViewById(R.id.close_candidates_pane);
+ mCloseCandidatesPane.setImageDrawable(
+ a.getDrawable(R.styleable.CandidateView_iconClosePane));
+ mCloseCandidatesPane.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ closeCandidatesPane();
}
- mTargetScrollX = scrollX;
- scrollTo(scrollX, getScrollY());
- hidePreview();
- invalidate();
- return true;
- }
+ });
+
+ a.recycle();
}
/**
- * A connection back to the service to communicate with the text field
+ * A connection back to the input method.
* @param listener
*/
- public void setService(LatinIME listener) {
- mService = listener;
+ public void setListener(Listener listener, View inputView) {
+ mListener = listener;
+ mKeyboardView = inputView.findViewById(R.id.keyboard_view);
+ mCandidatesPane = FrameLayoutCompatUtils.getPlacer(
+ (ViewGroup)inputView.findViewById(R.id.candidates_pane));
+ mCandidatesPane.setOnClickListener(this);
+ mCandidatesPaneContainer = (ViewGroup)inputView.findViewById(
+ R.id.candidates_pane_container);
}
-
- @Override
- public int computeHorizontalScrollRange() {
- return mTotalWidth;
+
+ public void setSuggestions(SuggestedWords suggestions) {
+ if (suggestions == null)
+ return;
+ mSuggestions = suggestions;
+ if (mShowingAutoCorrectionInverted) {
+ mHandler.postUpdateSuggestions();
+ } else {
+ updateSuggestions();
+ }
}
- /**
- * If the canvas is null, then only touch calculations are performed to pick the target
- * candidate.
- */
- @Override
- protected void onDraw(Canvas canvas) {
- if (canvas != null) {
- super.onDraw(canvas);
+ private CharSequence getStyledCandidateWord(CharSequence word, boolean isAutoCorrect) {
+ if (!isAutoCorrect)
+ return word;
+ final Spannable spannedWord = new SpannableString(word);
+ if ((mAutoCorrectHighlight & AUTO_CORRECT_BOLD) != 0)
+ spannedWord.setSpan(BOLD_SPAN, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ if ((mAutoCorrectHighlight & AUTO_CORRECT_UNDERLINE) != 0)
+ spannedWord.setSpan(UNDERLINE_SPAN, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ return spannedWord;
+ }
+
+ private int getCandidateTextColor(boolean isAutoCorrect, boolean isSuggestedCandidate,
+ SuggestedWordInfo info) {
+ final int color;
+ if (isAutoCorrect) {
+ color = mColorAutoCorrect;
+ } else if (isSuggestedCandidate) {
+ color = mColorSuggestedCandidate;
+ } else {
+ color = mColorTypedWord;
}
- mTotalWidth = 0;
-
- final int height = getHeight();
- if (mBgPadding == null) {
- mBgPadding = new Rect(0, 0, 0, 0);
- if (getBackground() != null) {
- getBackground().getPadding(mBgPadding);
- }
- mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(),
- mDivider.getIntrinsicHeight());
+ if (info != null && info.isPreviousSuggestedWord()) {
+ final int newAlpha = (int)(Color.alpha(color) * 0.5f);
+ return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
+ } else {
+ return color;
}
+ }
- final int count = mSuggestions.size();
- final Rect bgPadding = mBgPadding;
- final Paint paint = mPaint;
- final int touchX = mTouchX;
- final int scrollX = getScrollX();
- final boolean scrolled = mScrolled;
- final boolean typedWordValid = mTypedWordValid;
- final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2;
-
- boolean existsAutoCompletion = false;
+ private void updateSuggestions() {
+ final SuggestedWords suggestions = mSuggestions;
+ final List<SuggestedWordInfo> suggestedWordInfoList = suggestions.mSuggestedWordInfoList;
+ clear();
+ final int paneWidth = getWidth();
+ final int dividerWidth = mDividers.get(0).getMeasuredWidth();
+ final int dividerHeight = mDividers.get(0).getMeasuredHeight();
int x = 0;
+ int y = 0;
+ int fromIndex = NUM_CANDIDATES_IN_STRIP;
+ final int count = Math.min(mWords.size(), suggestions.size());
+ closeCandidatesPane();
+ mExpandCandidatesPane.setEnabled(count >= NUM_CANDIDATES_IN_STRIP);
for (int i = 0; i < count; i++) {
- CharSequence suggestion = mSuggestions.get(i);
+ final CharSequence suggestion = suggestions.getWord(i);
if (suggestion == null) continue;
- final int wordLength = suggestion.length();
-
- paint.setColor(mColorNormal);
- if (mHaveMinimalSuggestion
- && ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) {
- paint.setTypeface(Typeface.DEFAULT_BOLD);
- paint.setColor(mColorRecommended);
- existsAutoCompletion = true;
- } else if (i != 0 || (wordLength == 1 && count > 1)) {
- // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 and
- // there are multiple suggestions, such as the default punctuation list.
- paint.setColor(mColorOther);
- }
- int wordWidth;
- if ((wordWidth = mWordWidth[i]) == 0) {
- float textWidth = paint.measureText(suggestion, 0, wordLength);
- wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2);
- mWordWidth[i] = wordWidth;
- }
- mWordX[i] = x;
-
- if (touchX != OUT_OF_BOUNDS_X_COORD && !scrolled
- && touchX + scrollX >= x && touchX + scrollX < x + wordWidth) {
- if (canvas != null && !mShowingAddToDictionary) {
- canvas.translate(x, 0);
- mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height);
- mSelectionHighlight.draw(canvas);
- canvas.translate(-x, 0);
- }
- mSelectedString = suggestion;
- mSelectedIndex = i;
+ final SuggestedWordInfo suggestionInfo = (suggestedWordInfoList != null)
+ ? suggestedWordInfoList.get(i) : null;
+ final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion
+ && ((i == 1 && !suggestions.mTypedWordValid)
+ || (i == 0 && suggestions.mTypedWordValid));
+ // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1
+ // and there are multiple suggestions, such as the default punctuation list.
+ // TODO: Need to revisit this logic with bigram suggestions
+ final boolean isSuggestedCandidate = (i != 0);
+ final boolean isPunctuationSuggestions = (suggestion.length() == 1 && count > 1);
+
+ final TextView word = mWords.get(i);
+ // TODO: Reorder candidates in strip as appropriate. The center candidate should hold
+ // the word when space is typed (valid typed word or auto corrected word).
+ word.setTextColor(getCandidateTextColor(isAutoCorrect,
+ isSuggestedCandidate || isPunctuationSuggestions, suggestionInfo));
+ word.setText(getStyledCandidateWord(suggestion, isAutoCorrect));
+ // TODO: call TextView.setTextScaleX() to fit the candidate in single line.
+ word.measure(UNSPECIFIED_MEASURESPEC, UNSPECIFIED_MEASURESPEC);
+ final int width = word.getMeasuredWidth();
+ final int height = word.getMeasuredHeight();
+
+ final TextView info;
+ if (DBG && suggestionInfo != null
+ && !TextUtils.isEmpty(suggestionInfo.getDebugString())) {
+ info = mInfos.get(i);
+ info.setText(suggestionInfo.getDebugString());
+ info.setVisibility(View.VISIBLE);
+ info.measure(UNSPECIFIED_MEASURESPEC, UNSPECIFIED_MEASURESPEC);
+ } else {
+ info = null;
}
- if (canvas != null) {
- canvas.drawText(suggestion, 0, wordLength, x + wordWidth / 2, y, paint);
- paint.setColor(mColorOther);
- canvas.translate(x + wordWidth, 0);
- // Draw a divider unless it's after the hint
- if (!(mShowingAddToDictionary && i == 1)) {
- mDivider.draw(canvas);
+ if (i < NUM_CANDIDATES_IN_STRIP) {
+ if (info != null) {
+ final int infoWidth = info.getMeasuredWidth();
+ FrameLayoutCompatUtils.placeViewAt(
+ info, x + width - infoWidth, y, infoWidth, info.getMeasuredHeight());
}
- canvas.translate(-x - wordWidth, 0);
+ } else {
+ // TODO: Handle overflow case.
+ if (dividerWidth + x + width >= paneWidth) {
+ centeringCandidates(fromIndex, i - 1, x, paneWidth);
+ x = 0;
+ y += mCandidateStripHeight;
+ fromIndex = i;
+ }
+ if (x != 0) {
+ final View divider = mDividers.get(i - NUM_CANDIDATES_IN_STRIP);
+ mCandidatesPane.addView(divider);
+ FrameLayoutCompatUtils.placeViewAt(
+ divider, x, y + (mCandidateStripHeight - dividerHeight) / 2,
+ dividerWidth, dividerHeight);
+ x += dividerWidth;
+ }
+ mCandidatesPane.addView(word);
+ FrameLayoutCompatUtils.placeViewAt(
+ word, x, y + (mCandidateStripHeight - height) / 2, width, height);
+ if (info != null) {
+ mCandidatesPane.addView(info);
+ final int infoWidth = info.getMeasuredWidth();
+ FrameLayoutCompatUtils.placeViewAt(
+ info, x + width - infoWidth, y, infoWidth, info.getMeasuredHeight());
+ }
+ x += width;
}
- paint.setTypeface(Typeface.DEFAULT);
- x += wordWidth;
}
- mService.onAutoCompletionStateChanged(existsAutoCompletion);
- mTotalWidth = x;
- if (mTargetScrollX != scrollX) {
- scrollToTarget();
+ if (x != 0) {
+ // Centering last candidates row.
+ centeringCandidates(fromIndex, count - 1, x, paneWidth);
}
}
-
- private void scrollToTarget() {
- int scrollX = getScrollX();
- if (mTargetScrollX > scrollX) {
- scrollX += SCROLL_PIXELS;
- if (scrollX >= mTargetScrollX) {
- scrollX = mTargetScrollX;
- scrollTo(scrollX, getScrollY());
- requestLayout();
- } else {
- scrollTo(scrollX, getScrollY());
- }
+
+ private void centeringCandidates(int from, int to, int width, int paneWidth) {
+ final ViewGroup pane = mCandidatesPane;
+ final int fromIndex = pane.indexOfChild(mWords.get(from));
+ final int toIndex;
+ if (mInfos.get(to).getParent() != null) {
+ toIndex = pane.indexOfChild(mInfos.get(to));
} else {
- scrollX -= SCROLL_PIXELS;
- if (scrollX <= mTargetScrollX) {
- scrollX = mTargetScrollX;
- scrollTo(scrollX, getScrollY());
- requestLayout();
- } else {
- scrollTo(scrollX, getScrollY());
- }
+ toIndex = pane.indexOfChild(mWords.get(to));
+ }
+ final int offset = (paneWidth - width) / 2;
+ for (int index = fromIndex; index <= toIndex; index++) {
+ offsetMargin(pane.getChildAt(index), offset, 0);
}
- invalidate();
}
-
- public void setSuggestions(List<CharSequence> suggestions, boolean completions,
- boolean typedWordValid, boolean haveMinimalSuggestion) {
- clear();
- if (suggestions != null) {
- int insertCount = Math.min(suggestions.size(), MAX_SUGGESTIONS);
- for (CharSequence suggestion : suggestions) {
- mSuggestions.add(suggestion);
- if (--insertCount == 0)
- break;
- }
+
+ private static void offsetMargin(View v, int dx, int dy) {
+ if (v == null)
+ return;
+ final ViewGroup.LayoutParams lp = v.getLayoutParams();
+ if (lp instanceof ViewGroup.MarginLayoutParams) {
+ final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp;
+ mlp.setMargins(mlp.leftMargin + dx, mlp.topMargin + dy, 0, 0);
}
- mShowingCompletions = completions;
- mTypedWordValid = typedWordValid;
- scrollTo(0, getScrollY());
- mTargetScrollX = 0;
- mHaveMinimalSuggestion = haveMinimalSuggestion;
- // Compute the total width
- onDraw(null);
- invalidate();
- requestLayout();
+ }
+
+ private void expandCandidatesPane() {
+ mExpandCandidatesPane.setVisibility(View.GONE);
+ mCloseCandidatesPane.setVisibility(View.VISIBLE);
+ mCandidatesPaneContainer.setMinimumHeight(mKeyboardView.getMeasuredHeight());
+ mCandidatesPaneContainer.setVisibility(View.VISIBLE);
+ mKeyboardView.setVisibility(View.GONE);
+ }
+
+ private void closeCandidatesPane() {
+ mExpandCandidatesPane.setVisibility(View.VISIBLE);
+ mCloseCandidatesPane.setVisibility(View.GONE);
+ mCandidatesPaneContainer.setVisibility(View.GONE);
+ mKeyboardView.setVisibility(View.VISIBLE);
+ }
+
+ public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) {
+ if ((mAutoCorrectHighlight & AUTO_CORRECT_INVERT) == 0)
+ return;
+ final TextView tv = mWords.get(1);
+ final Spannable word = new SpannableString(autoCorrectedWord);
+ final int wordLength = word.length();
+ word.setSpan(mInvertedBackgroundColorSpan, 0, wordLength,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ word.setSpan(mInvertedForegroundColorSpan, 0, wordLength,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ tv.setText(word);
+ mShowingAutoCorrectionInverted = true;
}
public boolean isShowingAddToDictionaryHint() {
@@ -340,11 +446,14 @@ public class CandidateView extends View {
}
public void showAddToDictionaryHint(CharSequence word) {
- ArrayList<CharSequence> suggestions = new ArrayList<CharSequence>();
- suggestions.add(word);
- suggestions.add(mAddToDictionaryHint);
- setSuggestions(suggestions, false, false, false);
+ SuggestedWords.Builder builder = new SuggestedWords.Builder()
+ .addWord(word)
+ .addWord(getContext().getText(R.string.hint_add_to_dictionary));
+ setSuggestions(builder.build());
mShowingAddToDictionary = true;
+ // Disable R.string.hint_add_to_dictionary button
+ TextView tv = mWords.get(1);
+ tv.setClickable(false);
}
public boolean dismissAddToDictionaryHint() {
@@ -353,135 +462,94 @@ public class CandidateView extends View {
return true;
}
- /* package */ List<CharSequence> getSuggestions() {
+ public SuggestedWords getSuggestions() {
return mSuggestions;
}
public void clear() {
- // Don't call mSuggestions.clear() because it's being used for logging
- // in LatinIME.pickSuggestionManually().
- mSuggestions.clear();
- mTouchX = OUT_OF_BOUNDS_X_COORD;
- mSelectedString = null;
- mSelectedIndex = -1;
mShowingAddToDictionary = false;
- invalidate();
- Arrays.fill(mWordWidth, 0);
- Arrays.fill(mWordX, 0);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent me) {
-
- if (mGestureDetector.onTouchEvent(me)) {
- return true;
- }
-
- int action = me.getAction();
- int x = (int) me.getX();
- int y = (int) me.getY();
- mTouchX = x;
-
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- invalidate();
- break;
- case MotionEvent.ACTION_MOVE:
- if (y <= 0) {
- // Fling up!?
- if (mSelectedString != null) {
- // If there are completions from the application, we don't change the state to
- // STATE_PICKED_SUGGESTION
- if (!mShowingCompletions) {
- // This "acceptedSuggestion" will not be counted as a word because
- // it will be counted in pickSuggestion instead.
- TextEntryState.acceptedSuggestion(mSuggestions.get(0),
- mSelectedString);
- }
- mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
- mSelectedString = null;
- mSelectedIndex = -1;
- }
- }
- break;
- case MotionEvent.ACTION_UP:
- if (!mScrolled) {
- if (mSelectedString != null) {
- if (mShowingAddToDictionary) {
- longPressFirstWord();
- clear();
- } else {
- if (!mShowingCompletions) {
- TextEntryState.acceptedSuggestion(mSuggestions.get(0),
- mSelectedString);
- }
- mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
- }
- }
- }
- mSelectedString = null;
- mSelectedIndex = -1;
- requestLayout();
- hidePreview();
- invalidate();
- break;
+ mShowingAutoCorrectionInverted = false;
+ for (int i = 0; i < NUM_CANDIDATES_IN_STRIP; i++) {
+ mWords.get(i).setText(null);
+ mInfos.get(i).setVisibility(View.GONE);
}
- return true;
+ mCandidatesPane.removeAllViews();
}
private void hidePreview() {
- mTouchX = OUT_OF_BOUNDS_X_COORD;
- mCurrentWordIndex = OUT_OF_BOUNDS_WORD_INDEX;
mPreviewPopup.dismiss();
}
-
- private void showPreview(int wordIndex, String altText) {
- int oldWordIndex = mCurrentWordIndex;
- mCurrentWordIndex = wordIndex;
- // If index changed or changing text
- if (oldWordIndex != mCurrentWordIndex || altText != null) {
- if (wordIndex == OUT_OF_BOUNDS_WORD_INDEX) {
- hidePreview();
- } else {
- CharSequence word = altText != null? altText : mSuggestions.get(wordIndex);
- mPreviewText.setText(word);
- mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- int wordWidth = (int) (mPaint.measureText(word, 0, word.length()) + X_GAP * 2);
- final int popupWidth = wordWidth
- + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight();
- final int popupHeight = mPreviewText.getMeasuredHeight();
- //mPreviewText.setVisibility(INVISIBLE);
- mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - getScrollX()
- + (mWordWidth[wordIndex] - wordWidth) / 2;
- mPopupPreviewY = - popupHeight;
- int [] offsetInWindow = new int[2];
- getLocationInWindow(offsetInWindow);
- if (mPreviewPopup.isShowing()) {
- mPreviewPopup.update(mPopupPreviewX, mPopupPreviewY + offsetInWindow[1],
- popupWidth, popupHeight);
- } else {
- mPreviewPopup.setWidth(popupWidth);
- mPreviewPopup.setHeight(popupHeight);
- mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupPreviewX,
- mPopupPreviewY + offsetInWindow[1]);
- }
- mPreviewText.setVisibility(VISIBLE);
- }
+
+ private void showPreview(int index, CharSequence word) {
+ if (TextUtils.isEmpty(word))
+ return;
+
+ final TextView previewText = mPreviewText;
+ previewText.setTextColor(mColorTypedWord);
+ previewText.setText(word);
+ previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ View v = mWords.get(index);
+ final int[] offsetInWindow = new int[2];
+ v.getLocationInWindow(offsetInWindow);
+ final int posX = offsetInWindow[0];
+ final int posY = offsetInWindow[1] - previewText.getMeasuredHeight();
+ final PopupWindow previewPopup = mPreviewPopup;
+ if (previewPopup.isShowing()) {
+ previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight());
+ } else {
+ previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY);
}
+ previewText.setVisibility(VISIBLE);
+ mHandler.postHidePreview();
}
- private void longPressFirstWord() {
- CharSequence word = mSuggestions.get(0);
- if (word.length() < 2) return;
- if (mService.addWordToDictionary(word.toString())) {
- showPreview(0, getContext().getResources().getString(R.string.added_word, word));
+ private void addToDictionary(CharSequence word) {
+ if (mListener.addWordToDictionary(word.toString())) {
+ showPreview(0, getContext().getString(R.string.added_word, word));
}
}
-
+
+ @Override
+ public boolean onLongClick(View view) {
+ final Object tag = view.getTag();
+ if (!(tag instanceof Integer))
+ return true;
+ final int index = (Integer) tag;
+ if (index >= mSuggestions.size())
+ return true;
+
+ final CharSequence word = mSuggestions.getWord(index);
+ if (word.length() < 2)
+ return false;
+ addToDictionary(word);
+ return true;
+ }
+
+ @Override
+ public void onClick(View view) {
+ final Object tag = view.getTag();
+ if (!(tag instanceof Integer))
+ return;
+ final int index = (Integer) tag;
+ if (index >= mSuggestions.size())
+ return;
+
+ final CharSequence word = mSuggestions.getWord(index);
+ if (mShowingAddToDictionary && index == 0) {
+ addToDictionary(word);
+ } else {
+ mListener.pickSuggestionManually(index, word);
+ }
+ // Because some punctuation letters are not treated as word separator depending on locale,
+ // {@link #setSuggestions} might not be called and candidates pane left opened.
+ closeCandidatesPane();
+ }
+
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
+ mHandler.cancelAllMessages();
hidePreview();
}
}
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
index 95a3b5c7d..66a041508 100644
--- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
@@ -21,14 +21,17 @@ import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.SystemClock;
+import android.provider.BaseColumns;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.util.Log;
+import com.android.inputmethod.keyboard.Keyboard;
+
public class ContactsDictionary extends ExpandableDictionary {
private static final String[] PROJECTION = {
- Contacts._ID,
+ BaseColumns._ID,
Contacts.DISPLAY_NAME,
};
@@ -37,7 +40,7 @@ public class ContactsDictionary extends ExpandableDictionary {
/**
* Frequency for contacts information into the dictionary
*/
- private static final int FREQUENCY_FOR_CONTACTS = 128;
+ private static final int FREQUENCY_FOR_CONTACTS = 40;
private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
private static final int INDEX_NAME = 1;
@@ -94,6 +97,14 @@ public class ContactsDictionary extends ExpandableDictionary {
mLastLoadedContacts = SystemClock.uptimeMillis();
}
+ @Override
+ public void getBigrams(final WordComposer codes, final CharSequence previousWord,
+ final WordCallback callback) {
+ // Do not return bigrams from Contacts when nothing was typed.
+ if (codes.size() <= 0) return;
+ super.getBigrams(codes, previousWord, callback);
+ }
+
private void addWords(Cursor cursor) {
clearDictionary();
@@ -103,7 +114,7 @@ public class ContactsDictionary extends ExpandableDictionary {
while (!cursor.isAfterLast()) {
String name = cursor.getString(INDEX_NAME);
- if (name != null) {
+ if (name != null && -1 == name.indexOf('@')) {
int len = name.length();
String prevWord = null;
@@ -114,8 +125,9 @@ public class ContactsDictionary extends ExpandableDictionary {
for (j = i + 1; j < len; j++) {
char c = name.charAt(j);
- if (!(c == '-' || c == '\'' ||
- Character.isLetter(c))) {
+ if (!(c == Keyboard.CODE_DASH
+ || c == Keyboard.CODE_SINGLE_QUOTE
+ || Character.isLetter(c))) {
break;
}
}
@@ -131,8 +143,6 @@ public class ContactsDictionary extends ExpandableDictionary {
if (wordLen < maxWordLength && wordLen > 1) {
super.addWord(word, FREQUENCY_FOR_CONTACTS);
if (!TextUtils.isEmpty(prevWord)) {
- // TODO Do not add email address
- // Not so critical
super.setBigram(prevWord, word,
FREQUENCY_FOR_CONTACTS_BIGRAM);
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIMEDebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index cba1a0af9..fd62d61c3 100644
--- a/java/src/com/android/inputmethod/latin/LatinIMEDebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -20,17 +20,20 @@ import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
+import android.os.Process;
import android.preference.CheckBoxPreference;
import android.preference.PreferenceActivity;
import android.util.Log;
-public class LatinIMEDebugSettings extends PreferenceActivity
+public class DebugSettings extends PreferenceActivity
implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = "LatinIMEDebugSettings";
+ private static final String TAG = "DebugSettings";
private static final String DEBUG_MODE_KEY = "debug_mode";
+ private boolean mServiceNeedsRestart = false;
private CheckBoxPreference mDebugMode;
+ private CheckBoxPreference mUseSpacebarLanguageSwitch;
@Override
protected void onCreate(Bundle icicle) {
@@ -39,15 +42,31 @@ public class LatinIMEDebugSettings extends PreferenceActivity
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
prefs.registerOnSharedPreferenceChangeListener(this);
+ mServiceNeedsRestart = false;
mDebugMode = (CheckBoxPreference) findPreference(DEBUG_MODE_KEY);
updateDebugMode();
}
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mServiceNeedsRestart) Process.killProcess(Process.myPid());
+ }
+
+ @Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if (key.equals(DEBUG_MODE_KEY)) {
if (mDebugMode != null) {
mDebugMode.setChecked(prefs.getBoolean(DEBUG_MODE_KEY, false));
updateDebugMode();
+ mServiceNeedsRestart = true;
+ }
+ } else if (key.equals(SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCH_KEY)) {
+ if (mUseSpacebarLanguageSwitch != null) {
+ mUseSpacebarLanguageSwitch.setChecked(
+ prefs.getBoolean(SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCH_KEY,
+ getResources().getBoolean(
+ R.bool.config_use_spacebar_language_switcher)));
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index d04bf57a7..c7737b9a2 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -20,7 +20,7 @@ package com.android.inputmethod.latin;
* Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
* strokes.
*/
-abstract public class Dictionary {
+public abstract class Dictionary {
/**
* Whether or not to replicate the typed word in the suggested list, even if it's valid.
*/
@@ -29,7 +29,7 @@ abstract public class Dictionary {
/**
* The weight to give to a word if it's length is the same as the number of typed characters.
*/
- protected static final int FULL_WORD_FREQ_MULTIPLIER = 2;
+ protected static final int FULL_WORD_SCORE_MULTIPLIER = 2;
public static enum DataType {
UNIGRAM, BIGRAM
@@ -42,17 +42,17 @@ abstract public class Dictionary {
public interface WordCallback {
/**
* Adds a word to a list of suggestions. The word is expected to be ordered based on
- * the provided frequency.
+ * the provided score.
* @param word the character array containing the word
* @param wordOffset starting offset of the word in the character array
* @param wordLength length of valid characters in the character array
- * @param frequency the frequency of occurence. This is normalized between 1 and 255, but
+ * @param score the score of occurrence. This is normalized between 1 and 255, but
* can exceed those limits
* @param dicTypeId of the dictionary where word was from
* @param dataType tells type of this data
* @return true if the word was added, false if no more words are required
*/
- boolean addWord(char[] word, int wordOffset, int wordLength, int frequency, int dicTypeId,
+ boolean addWord(char[] word, int wordOffset, int wordLength, int score, int dicTypeId,
DataType dataType);
}
@@ -61,27 +61,19 @@ abstract public class Dictionary {
* words are added through the callback object.
* @param composer the key sequence to match
* @param callback the callback object to send matched words to as possible candidates
- * @param nextLettersFrequencies array of frequencies of next letters that could follow the
- * word so far. For instance, "bracke" can be followed by "t", so array['t'] will have
- * a non-zero value on returning from this method.
- * Pass in null if you don't want the dictionary to look up next letters.
- * @see WordCallback#addWord(char[], int, int)
+ * @see WordCallback#addWord(char[], int, int, int, int, DataType)
*/
- abstract public void getWords(final WordComposer composer, final WordCallback callback,
- int[] nextLettersFrequencies);
+ abstract public void getWords(final WordComposer composer, final WordCallback callback);
/**
* Searches for pairs in the bigram dictionary that matches the previous word and all the
* possible words following are added through the callback object.
* @param composer the key sequence to match
+ * @param previousWord the word before
* @param callback the callback object to send possible word following previous word
- * @param nextLettersFrequencies array of frequencies of next letters that could follow the
- * word so far. For instance, "bracke" can be followed by "t", so array['t'] will have
- * a non-zero value on returning from this method.
- * Pass in null if you don't want the dictionary to look up next letters.
*/
public void getBigrams(final WordComposer composer, final CharSequence previousWord,
- final WordCallback callback, int[] nextLettersFrequencies) {
+ final WordCallback callback) {
// empty base implementation
}
@@ -116,5 +108,6 @@ abstract public class Dictionary {
* Override to clean up any resources.
*/
public void close() {
+ // empty base implementation
}
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
new file mode 100644
index 000000000..5e7de3e6b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -0,0 +1,71 @@
+/*
+ * 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.latin;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Class for a collection of dictionaries that behave like one dictionary.
+ */
+public class DictionaryCollection extends Dictionary {
+
+ protected final List<Dictionary> mDictionaries;
+
+ public DictionaryCollection() {
+ mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+ }
+
+ public DictionaryCollection(Dictionary... dictionaries) {
+ mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ }
+
+ public DictionaryCollection(Collection<Dictionary> dictionaries) {
+ mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ }
+
+ @Override
+ public void getWords(final WordComposer composer, final WordCallback callback) {
+ for (final Dictionary dict : mDictionaries)
+ dict.getWords(composer, callback);
+ }
+
+ @Override
+ public void getBigrams(final WordComposer composer, final CharSequence previousWord,
+ final WordCallback callback) {
+ for (final Dictionary dict : mDictionaries)
+ dict.getBigrams(composer, previousWord, callback);
+ }
+
+ @Override
+ public boolean isValidWord(CharSequence word) {
+ for (int i = mDictionaries.size() - 1; i >= 0; --i)
+ if (mDictionaries.get(i).isValidWord(word)) return true;
+ return false;
+ }
+
+ @Override
+ public void close() {
+ for (final Dictionary dict : mDictionaries)
+ dict.close();
+ }
+
+ public void addDictionary(Dictionary newDict) {
+ mDictionaries.add(newDict);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
new file mode 100644
index 000000000..bba331868
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -0,0 +1,177 @@
+/*
+ * 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.latin;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.util.Log;
+
+import java.io.File;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Factory for dictionary instances.
+ */
+public class DictionaryFactory {
+
+ private static String TAG = DictionaryFactory.class.getSimpleName();
+
+ /**
+ * Initializes a dictionary from a dictionary pack.
+ *
+ * This searches for a content provider providing a dictionary pack for the specified
+ * locale. If none is found, it falls back to using the resource passed as fallBackResId
+ * as a dictionary.
+ * @param context application context for reading resources
+ * @param locale the locale for which to create the dictionary
+ * @param fallbackResId the id of the resource to use as a fallback if no pack is found
+ * @return an initialized instance of Dictionary
+ */
+ public static Dictionary createDictionaryFromManager(Context context, Locale locale,
+ int fallbackResId) {
+ if (null == locale) {
+ Log.e(TAG, "No locale defined for dictionary");
+ return new DictionaryCollection(createBinaryDictionary(context, fallbackResId));
+ }
+
+ final List<Dictionary> dictList = new LinkedList<Dictionary>();
+ for (final AssetFileAddress f : BinaryDictionaryGetter.getDictionaryFiles(locale,
+ context, fallbackResId)) {
+ dictList.add(new BinaryDictionary(context, f.mFilename, f.mOffset, f.mLength, null));
+ }
+
+ if (null == dictList) return null;
+ return new DictionaryCollection(dictList);
+ }
+
+ /**
+ * Initializes a dictionary from a raw resource file
+ * @param context application context for reading resources
+ * @param resId the resource containing the raw binary dictionary
+ * @return an initialized instance of BinaryDictionary
+ */
+ protected static BinaryDictionary createBinaryDictionary(Context context, int resId) {
+ AssetFileDescriptor afd = null;
+ try {
+ afd = context.getResources().openRawResourceFd(resId);
+ if (afd == null) {
+ Log.e(TAG, "Found the resource but it is compressed. resId=" + resId);
+ return null;
+ }
+ if (!isFullDictionary(afd)) return null;
+ final String sourceDir = context.getApplicationInfo().sourceDir;
+ final File packagePath = new File(sourceDir);
+ // TODO: Come up with a way to handle a directory.
+ if (!packagePath.isFile()) {
+ Log.e(TAG, "sourceDir is not a file: " + sourceDir);
+ return null;
+ }
+ return new BinaryDictionary(context,
+ sourceDir, afd.getStartOffset(), afd.getLength(), null);
+ } catch (android.content.res.Resources.NotFoundException e) {
+ Log.e(TAG, "Could not find the resource. resId=" + resId);
+ return null;
+ } finally {
+ if (null != afd) {
+ try {
+ afd.close();
+ } catch (java.io.IOException e) {
+ /* IOException on close ? What am I supposed to do ? */
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a dictionary from passed data. This is intended for unit tests only.
+ * @param context the test context to create this data from.
+ * @param dictionary the file to read
+ * @param startOffset the offset in the file where the data starts
+ * @param length the length of the data
+ * @param flagArray the flags to use with this data for testing
+ * @return the created dictionary, or null.
+ */
+ public static Dictionary createDictionaryForTest(Context context, File dictionary,
+ long startOffset, long length, Flag[] flagArray) {
+ if (dictionary.isFile()) {
+ return new BinaryDictionary(context, dictionary.getAbsolutePath(), startOffset, length,
+ flagArray);
+ } else {
+ Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
+ return null;
+ }
+ }
+
+ /**
+ * Find out whether a dictionary is available for this locale.
+ * @param context the context on which to check resources.
+ * @param locale the locale to check for.
+ * @return whether a (non-placeholder) dictionary is available or not.
+ */
+ public static boolean isDictionaryAvailable(Context context, Locale locale) {
+ final Resources res = context.getResources();
+ final Locale saveLocale = Utils.setSystemLocale(res, locale);
+
+ final int resourceId = Utils.getMainDictionaryResourceId(res);
+ final AssetFileDescriptor afd = res.openRawResourceFd(resourceId);
+ final boolean hasDictionary = isFullDictionary(afd);
+ try {
+ if (null != afd) afd.close();
+ } catch (java.io.IOException e) {
+ /* Um, what can we do here exactly? */
+ }
+
+ Utils.setSystemLocale(res, saveLocale);
+ return hasDictionary;
+ }
+
+ // TODO: Do not use the size of the dictionary as an unique dictionary ID.
+ public static Long getDictionaryId(Context context, Locale locale) {
+ final Resources res = context.getResources();
+ final Locale saveLocale = Utils.setSystemLocale(res, locale);
+
+ final int resourceId = Utils.getMainDictionaryResourceId(res);
+ final AssetFileDescriptor afd = res.openRawResourceFd(resourceId);
+ final Long size = (afd != null && afd.getLength() > PLACEHOLDER_LENGTH)
+ ? afd.getLength()
+ : null;
+ try {
+ if (null != afd) afd.close();
+ } catch (java.io.IOException e) {
+ }
+
+ Utils.setSystemLocale(res, saveLocale);
+ return size;
+ }
+
+ // TODO: Find the Right Way to find out whether the resource is a placeholder or not.
+ // Suggestion : strip the locale, open the placeholder file and store its offset.
+ // Upon opening the file, if it's the same offset, then it's the placeholder.
+ private static final long PLACEHOLDER_LENGTH = 34;
+ /**
+ * Finds out whether the data pointed out by an AssetFileDescriptor is a full
+ * dictionary (as opposed to null, or to a place holder).
+ * @param afd the file descriptor to test, or null
+ * @return true if the dictionary is a real full dictionary, false if it's null or a placeholder
+ */
+ protected static boolean isFullDictionary(final AssetFileDescriptor afd) {
+ return (afd != null && afd.getLength() > PLACEHOLDER_LENGTH);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
new file mode 100644
index 000000000..9d30af84b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
@@ -0,0 +1,89 @@
+/*
+ * 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.latin;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.net.Uri;
+
+/**
+ * Takes action to reload the necessary data when a dictionary pack was added/removed.
+ */
+public class DictionaryPackInstallBroadcastReceiver extends BroadcastReceiver {
+
+ final LatinIME mService;
+ /**
+ * The action of the intent for publishing that new dictionary data is available.
+ */
+ /* package */ static final String NEW_DICTIONARY_INTENT_ACTION =
+ "com.android.inputmethod.latin.dictionarypack.newdict";
+
+ public DictionaryPackInstallBroadcastReceiver(final LatinIME service) {
+ mService = service;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ final PackageManager manager = context.getPackageManager();
+
+ // We need to reread the dictionary if a new dictionary package is installed.
+ if (action.equals(Intent.ACTION_PACKAGE_ADDED)) {
+ final Uri packageUri = intent.getData();
+ if (null == packageUri) return; // No package name : we can't do anything
+ final String packageName = packageUri.getSchemeSpecificPart();
+ if (null == packageName) return;
+ final PackageInfo packageInfo;
+ try {
+ packageInfo = manager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS);
+ } catch (android.content.pm.PackageManager.NameNotFoundException e) {
+ return; // No package info : we can't do anything
+ }
+ final ProviderInfo[] providers = packageInfo.providers;
+ if (null == providers) return; // No providers : it is not a dictionary.
+
+ // Search for some dictionary pack in the just-installed package. If found, reread.
+ for (ProviderInfo info : providers) {
+ if (BinaryDictionary.DICTIONARY_PACK_AUTHORITY.equals(info.authority)) {
+ mService.resetSuggestMainDict();
+ return;
+ }
+ }
+ // If we come here none of the authorities matched the one we searched for.
+ // We can exit safely.
+ return;
+ } else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
+ && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ // When the dictionary package is removed, we need to reread dictionary (to use the
+ // next-priority one, or stop using a dictionary at all if this was the only one,
+ // since this is the user request).
+ // If we are replacing the package, we will receive ADDED right away so no need to
+ // remove the dictionary at the moment, since we will do it when we receive the
+ // ADDED broadcast.
+
+ // TODO: Only reload dictionary on REMOVED when the removed package is the one we
+ // read dictionary from?
+ mService.resetSuggestMainDict();
+ } else if (action.equals(NEW_DICTIONARY_INTENT_ACTION)) {
+ mService.resetSuggestMainDict();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/EditingUtil.java b/java/src/com/android/inputmethod/latin/EditingUtils.java
index 781d7fd4a..e56aa695d 100644
--- a/java/src/com/android/inputmethod/latin/EditingUtil.java
+++ b/java/src/com/android/inputmethod/latin/EditingUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 Google Inc.
+ * 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
@@ -16,30 +16,27 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.compat.InputConnectionCompatUtils;
+
import android.text.TextUtils;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.util.regex.Pattern;
/**
* Utility methods to deal with editing text through an InputConnection.
*/
-public class EditingUtil {
+public class EditingUtils {
/**
* Number of characters we want to look back in order to identify the previous word
*/
private static final int LOOKBACK_CHARACTER_NUM = 15;
- // Cache Method pointers
- private static boolean sMethodsInitialized;
- private static Method sMethodGetSelectedText;
- private static Method sMethodSetComposingRegion;
-
- private EditingUtil() {};
+ private EditingUtils() {
+ // Unintentional empty constructor for singleton.
+ }
/**
* Append newText to the text field represented by connection.
@@ -54,14 +51,15 @@ public class EditingUtil {
connection.finishComposingText();
// Add a space if the field already has text.
+ String text = newText;
CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
if (charBeforeCursor != null
&& !charBeforeCursor.equals(" ")
&& (charBeforeCursor.length() > 0)) {
- newText = " " + newText;
+ text = " " + text;
}
- connection.setComposingText(newText, 1);
+ connection.setComposingText(text, 1);
}
private static int getCursorPosition(InputConnection connection) {
@@ -75,34 +73,30 @@ public class EditingUtil {
/**
* @param connection connection to the current text field.
- * @param sep characters which may separate words
- * @param range the range object to store the result into
+ * @param separators characters which may separate words
* @return the word that surrounds the cursor, including up to one trailing
* separator. For example, if the field contains "he|llo world", where |
* represents the cursor, then "hello " will be returned.
*/
- public static String getWordAtCursor(
- InputConnection connection, String separators, Range range) {
- Range r = getWordRangeAtCursor(connection, separators, range);
- return (r == null) ? null : r.word;
+ public static String getWordAtCursor(InputConnection connection, String separators) {
+ Range r = getWordRangeAtCursor(connection, separators);
+ return (r == null) ? null : r.mWord;
}
/**
* Removes the word surrounding the cursor. Parameters are identical to
* getWordAtCursor.
*/
- public static void deleteWordAtCursor(
- InputConnection connection, String separators) {
-
- Range range = getWordRangeAtCursor(connection, separators, null);
+ public static void deleteWordAtCursor(InputConnection connection, String separators) {
+ Range range = getWordRangeAtCursor(connection, separators);
if (range == null) return;
connection.finishComposingText();
// Move cursor to beginning of word, to avoid crash when cursor is outside
// of valid range after deleting text.
- int newCursor = getCursorPosition(connection) - range.charsBefore;
+ int newCursor = getCursorPosition(connection) - range.mCharsBefore;
connection.setSelection(newCursor, newCursor);
- connection.deleteSurroundingText(0, range.charsBefore + range.charsAfter);
+ connection.deleteSurroundingText(0, range.mCharsBefore + range.mCharsAfter);
}
/**
@@ -110,31 +104,28 @@ public class EditingUtil {
*/
public static class Range {
/** Characters before selection start */
- public int charsBefore;
+ public final int mCharsBefore;
/**
* Characters after selection start, including one trailing word
* separator.
*/
- public int charsAfter;
+ public final int mCharsAfter;
/** The actual characters that make up a word */
- public String word;
-
- public Range() {}
+ public final String mWord;
public Range(int charsBefore, int charsAfter, String word) {
if (charsBefore < 0 || charsAfter < 0) {
throw new IndexOutOfBoundsException();
}
- this.charsBefore = charsBefore;
- this.charsAfter = charsAfter;
- this.word = word;
+ this.mCharsBefore = charsBefore;
+ this.mCharsAfter = charsAfter;
+ this.mWord = word;
}
}
- private static Range getWordRangeAtCursor(
- InputConnection connection, String sep, Range range) {
+ private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
if (connection == null || sep == null) {
return null;
}
@@ -150,18 +141,15 @@ public class EditingUtil {
// Find last word separator after the cursor
int end = -1;
- while (++end < after.length() && !isWhitespace(after.charAt(end), sep));
+ while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
+ // Nothing to do here.
+ }
int cursor = getCursorPosition(connection);
if (start >= 0 && cursor + end <= after.length() + before.length()) {
String word = before.toString().substring(start, before.length())
+ after.toString().substring(0, end);
-
- Range returnRange = range != null? range : new Range();
- returnRange.charsBefore = before.length() - start;
- returnRange.charsAfter = end;
- returnRange.word = word;
- return returnRange;
+ return new Range(before.length() - start, end, word);
}
return null;
@@ -173,29 +161,74 @@ public class EditingUtil {
private static final Pattern spaceRegex = Pattern.compile("\\s+");
+
public static CharSequence getPreviousWord(InputConnection connection,
String sentenceSeperators) {
//TODO: Should fix this. This could be slow!
CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
- if (prev == null) {
- return null;
- }
+ return getPreviousWord(prev, sentenceSeperators);
+ }
+
+ // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
+ // Also, it won't return words that end in a separator.
+ // Example :
+ // "abc def|" -> abc
+ // "abc def |" -> abc
+ // "abc def. |" -> abc
+ // "abc def . |" -> def
+ // "abc|" -> null
+ // "abc |" -> null
+ // "abc. def|" -> null
+ public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
+ if (prev == null) return null;
String[] w = spaceRegex.split(prev);
- if (w.length >= 2 && w[w.length-2].length() > 0) {
- char lastChar = w[w.length-2].charAt(w[w.length-2].length() -1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) {
- return null;
- }
- return w[w.length-2];
- } else {
- return null;
- }
+
+ // If we can't find two words, or we found an empty word, return null.
+ if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
+
+ // If ends in a separator, return null
+ char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
+ if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+ return w[w.length - 2];
+ }
+
+ public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
+ final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+ return getThisWord(prev, sentenceSeperators);
+ }
+
+ // Get the word immediately before the cursor, even if there is whitespace between it and
+ // the cursor - but not if there is punctuation.
+ // Example :
+ // "abc def|" -> def
+ // "abc def |" -> def
+ // "abc def. |" -> null
+ // "abc def . |" -> null
+ public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
+ if (prev == null) return null;
+ String[] w = spaceRegex.split(prev);
+
+ // No word : return null
+ if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
+
+ // If ends in a separator, return null
+ char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
+ if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+ return w[w.length - 1];
}
public static class SelectedWord {
- public int start;
- public int end;
- public CharSequence word;
+ public final int mStart;
+ public final int mEnd;
+ public final CharSequence mWord;
+
+ public SelectedWord(int start, int end, CharSequence word) {
+ mStart = start;
+ mEnd = end;
+ mWord = word;
+ }
}
/**
@@ -223,14 +256,10 @@ public class EditingUtil {
int selStart, int selEnd, String wordSeparators) {
if (selStart == selEnd) {
// There is just a cursor, so get the word at the cursor
- EditingUtil.Range range = new EditingUtil.Range();
- CharSequence touching = getWordAtCursor(ic, wordSeparators, range);
- if (!TextUtils.isEmpty(touching)) {
- SelectedWord selWord = new SelectedWord();
- selWord.word = touching;
- selWord.start = selStart - range.charsBefore;
- selWord.end = selEnd + range.charsAfter;
- return selWord;
+ EditingUtils.Range range = getWordRangeAtCursor(ic, wordSeparators);
+ if (range != null && !TextUtils.isEmpty(range.mWord)) {
+ return new SelectedWord(selStart - range.mCharsBefore, selEnd + range.mCharsAfter,
+ range.mWord);
}
} else {
// Is the previous character empty or a word separator? If not, return null.
@@ -246,7 +275,8 @@ public class EditingUtil {
}
// Extract the selection alone
- CharSequence touching = getSelectedText(ic, selStart, selEnd);
+ CharSequence touching = InputConnectionCompatUtils.getSelectedText(
+ ic, selStart, selEnd);
if (TextUtils.isEmpty(touching)) return null;
// Is any part of the selection a separator? If so, return null.
final int length = touching.length();
@@ -256,82 +286,8 @@ public class EditingUtil {
}
}
// Prepare the selected word
- SelectedWord selWord = new SelectedWord();
- selWord.start = selStart;
- selWord.end = selEnd;
- selWord.word = touching;
- return selWord;
+ return new SelectedWord(selStart, selEnd, touching);
}
return null;
}
-
- /**
- * Cache method pointers for performance
- */
- private static void initializeMethodsForReflection() {
- try {
- // These will either both exist or not, so no need for separate try/catch blocks.
- // If other methods are added later, use separate try/catch blocks.
- sMethodGetSelectedText = InputConnection.class.getMethod("getSelectedText", int.class);
- sMethodSetComposingRegion = InputConnection.class.getMethod("setComposingRegion",
- int.class, int.class);
- } catch (NoSuchMethodException exc) {
- // Ignore
- }
- sMethodsInitialized = true;
- }
-
- /**
- * Returns the selected text between the selStart and selEnd positions.
- */
- private static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) {
- // Use reflection, for backward compatibility
- CharSequence result = null;
- if (!sMethodsInitialized) {
- initializeMethodsForReflection();
- }
- if (sMethodGetSelectedText != null) {
- try {
- result = (CharSequence) sMethodGetSelectedText.invoke(ic, 0);
- return result;
- } catch (InvocationTargetException exc) {
- // Ignore
- } catch (IllegalArgumentException e) {
- // Ignore
- } catch (IllegalAccessException e) {
- // Ignore
- }
- }
- // Reflection didn't work, try it the poor way, by moving the cursor to the start,
- // getting the text after the cursor and moving the text back to selected mode.
- // TODO: Verify that this works properly in conjunction with
- // LatinIME#onUpdateSelection
- ic.setSelection(selStart, selEnd);
- result = ic.getTextAfterCursor(selEnd - selStart, 0);
- ic.setSelection(selStart, selEnd);
- return result;
- }
-
- /**
- * Tries to set the text into composition mode if there is support for it in the framework.
- */
- public static void underlineWord(InputConnection ic, SelectedWord word) {
- // Use reflection, for backward compatibility
- // If method not found, there's nothing we can do. It still works but just wont underline
- // the word.
- if (!sMethodsInitialized) {
- initializeMethodsForReflection();
- }
- if (sMethodSetComposingRegion != null) {
- try {
- sMethodSetComposingRegion.invoke(ic, word.start, word.end);
- } catch (InvocationTargetException exc) {
- // Ignore
- } catch (IllegalArgumentException e) {
- // Ignore
- } catch (IllegalAccessException e) {
- // Ignore
- }
- }
- }
}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index e954c0818..97a4a1816 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -16,11 +16,13 @@
package com.android.inputmethod.latin;
-import java.util.LinkedList;
-
import android.content.Context;
import android.os.AsyncTask;
+import com.android.inputmethod.keyboard.Keyboard;
+
+import java.util.LinkedList;
+
/**
* Base class for an in-memory dictionary that can grow dynamically and can
* be searched for suggestions and valid words.
@@ -32,15 +34,14 @@ public class ExpandableDictionary extends Dictionary {
*/
protected static final int MAX_WORD_LENGTH = 32;
+ // Bigram frequency is a fixed point number with 1 meaning 1.2 and 255 meaning 1.8.
+ protected static final int BIGRAM_MAX_FREQUENCY = 255;
+
private Context mContext;
private char[] mWordBuilder = new char[MAX_WORD_LENGTH];
private int mDicTypeId;
private int mMaxDepth;
private int mInputLength;
- private int[] mNextLettersFrequencies;
- private StringBuilder sb = new StringBuilder(MAX_WORD_LENGTH);
-
- private static final char QUOTE = '\'';
private boolean mRequiresReload;
@@ -49,53 +50,66 @@ public class ExpandableDictionary extends Dictionary {
// Use this lock before touching mUpdatingDictionary & mRequiresDownload
private Object mUpdatingLock = new Object();
- static class Node {
- char code;
- int frequency;
- boolean terminal;
- Node parent;
- NodeArray children;
- LinkedList<NextWord> ngrams; // Supports ngram
+ private static class Node {
+ char mCode;
+ int mFrequency;
+ boolean mTerminal;
+ Node mParent;
+ NodeArray mChildren;
+ LinkedList<NextWord> mNGrams; // Supports ngram
}
- static class NodeArray {
- Node[] data;
- int length = 0;
+ private static class NodeArray {
+ Node[] mData;
+ int mLength = 0;
private static final int INCREMENT = 2;
NodeArray() {
- data = new Node[INCREMENT];
+ mData = new Node[INCREMENT];
}
void add(Node n) {
- if (length + 1 > data.length) {
- Node[] tempData = new Node[length + INCREMENT];
- if (length > 0) {
- System.arraycopy(data, 0, tempData, 0, length);
+ if (mLength + 1 > mData.length) {
+ Node[] tempData = new Node[mLength + INCREMENT];
+ if (mLength > 0) {
+ System.arraycopy(mData, 0, tempData, 0, mLength);
}
- data = tempData;
+ mData = tempData;
}
- data[length++] = n;
+ mData[mLength++] = n;
}
}
- static class NextWord {
- Node word;
- NextWord nextWord;
- int frequency;
+ private static class NextWord {
+ public final Node mWord;
+ private int mFrequency;
- NextWord(Node word, int frequency) {
- this.word = word;
- this.frequency = frequency;
+ public NextWord(Node word, int frequency) {
+ mWord = word;
+ mFrequency = frequency;
+ }
+
+ public int getFrequency() {
+ return mFrequency;
}
- }
+ public int setFrequency(int freq) {
+ mFrequency = freq;
+ return mFrequency;
+ }
+
+ public int addFrequency(int add) {
+ mFrequency += add;
+ if (mFrequency > BIGRAM_MAX_FREQUENCY) mFrequency = BIGRAM_MAX_FREQUENCY;
+ return mFrequency;
+ }
+ }
private NodeArray mRoots;
private int[][] mCodes;
- ExpandableDictionary(Context context, int dicTypeId) {
+ public ExpandableDictionary(Context context, int dicTypeId) {
mContext = context;
clearDictionary();
mCodes = new int[MAX_WORD_LENGTH][];
@@ -128,13 +142,14 @@ public class ExpandableDictionary extends Dictionary {
/** Override to load your dictionary here, on a background thread. */
public void loadDictionaryAsync() {
+ // empty base implementation
}
- Context getContext() {
+ public Context getContext() {
return mContext;
}
-
- int getMaxWordLength() {
+
+ public int getMaxWordLength() {
return MAX_WORD_LENGTH;
}
@@ -145,40 +160,40 @@ public class ExpandableDictionary extends Dictionary {
private void addWordRec(NodeArray children, final String word, final int depth,
final int frequency, Node parentNode) {
final int wordLength = word.length();
+ if (wordLength <= depth) return;
final char c = word.charAt(depth);
// Does children have the current character?
- final int childrenLength = children.length;
+ final int childrenLength = children.mLength;
Node childNode = null;
boolean found = false;
for (int i = 0; i < childrenLength; i++) {
- childNode = children.data[i];
- if (childNode.code == c) {
+ childNode = children.mData[i];
+ if (childNode.mCode == c) {
found = true;
break;
}
}
if (!found) {
childNode = new Node();
- childNode.code = c;
- childNode.parent = parentNode;
+ childNode.mCode = c;
+ childNode.mParent = parentNode;
children.add(childNode);
}
if (wordLength == depth + 1) {
// Terminate this word
- childNode.terminal = true;
- childNode.frequency = Math.max(frequency, childNode.frequency);
- if (childNode.frequency > 255) childNode.frequency = 255;
+ childNode.mTerminal = true;
+ childNode.mFrequency = Math.max(frequency, childNode.mFrequency);
+ if (childNode.mFrequency > 255) childNode.mFrequency = 255;
return;
}
- if (childNode.children == null) {
- childNode.children = new NodeArray();
+ if (childNode.mChildren == null) {
+ childNode.mChildren = new NodeArray();
}
- addWordRec(childNode.children, word, depth + 1, frequency, childNode);
+ addWordRec(childNode.mChildren, word, depth + 1, frequency, childNode);
}
@Override
- public void getWords(final WordComposer codes, final WordCallback callback,
- int[] nextLettersFrequencies) {
+ public void getWords(final WordComposer codes, final WordCallback callback) {
synchronized (mUpdatingLock) {
// If we need to update, start off a background task
if (mRequiresReload) startDictionaryLoadingTaskLocked();
@@ -187,7 +202,6 @@ public class ExpandableDictionary extends Dictionary {
}
mInputLength = codes.size();
- mNextLettersFrequencies = nextLettersFrequencies;
if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
// Cache the codes so that we don't have to lookup an array list
for (int i = 0; i < mInputLength; i++) {
@@ -214,9 +228,20 @@ public class ExpandableDictionary extends Dictionary {
/**
* Returns the word's frequency or -1 if not found
*/
- public int getWordFrequency(CharSequence word) {
+ protected int getWordFrequency(CharSequence word) {
+ // Case-sensitive search
Node node = searchNode(mRoots, word, 0, word.length());
- return (node == null) ? -1 : node.frequency;
+ return (node == null) ? -1 : node.mFrequency;
+ }
+
+ private static int computeSkippedWordFinalFreq(int freq, int snr, int inputLength) {
+ // The computation itself makes sense for >= 2, but the == 2 case returns 0
+ // anyway so we may as well test against 3 instead and return the constant
+ if (inputLength >= 3) {
+ return (freq * snr * (inputLength - 2)) / (inputLength - 1);
+ } else {
+ return 0;
+ }
}
/**
@@ -232,16 +257,17 @@ public class ExpandableDictionary extends Dictionary {
* @param completion whether the traversal is now in completion mode - meaning that we've
* exhausted the input and we're looking for all possible suffixes.
* @param snr current weight of the word being formed
- * @param inputIndex position in the input characters. This can be off from the depth in
+ * @param inputIndex position in the input characters. This can be off from the depth in
* case we skip over some punctuations such as apostrophe in the traversal. That is, if you type
* "wouldve", it could be matching "would've", so the depth will be one more than the
* inputIndex
* @param callback the callback class for adding a word
*/
- protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word,
+ // TODO: Share this routine with the native code for BinaryDictionary
+ protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word,
final int depth, boolean completion, int snr, int inputIndex, int skipPos,
WordCallback callback) {
- final int count = roots.length;
+ final int count = roots.mLength;
final int codeSize = mInputLength;
// Optimization: Prune out words that are too long compared to how much was typed.
if (depth > mMaxDepth) {
@@ -255,34 +281,36 @@ public class ExpandableDictionary extends Dictionary {
}
for (int i = 0; i < count; i++) {
- final Node node = roots.data[i];
- final char c = node.code;
+ final Node node = roots.mData[i];
+ final char c = node.mCode;
final char lowerC = toLowerCase(c);
- final boolean terminal = node.terminal;
- final NodeArray children = node.children;
- final int freq = node.frequency;
+ final boolean terminal = node.mTerminal;
+ final NodeArray children = node.mChildren;
+ final int freq = node.mFrequency;
if (completion) {
word[depth] = c;
if (terminal) {
- if (!callback.addWord(word, 0, depth + 1, freq * snr, mDicTypeId,
- DataType.UNIGRAM)) {
- return;
+ final int finalFreq;
+ if (skipPos < 0) {
+ finalFreq = freq * snr;
+ } else {
+ finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength);
}
- // Add to frequency of next letters for predictive correction
- if (mNextLettersFrequencies != null && depth >= inputIndex && skipPos < 0
- && mNextLettersFrequencies.length > word[inputIndex]) {
- mNextLettersFrequencies[word[inputIndex]]++;
+ if (!callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId,
+ DataType.UNIGRAM)) {
+ return;
}
}
if (children != null) {
getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
skipPos, callback);
}
- } else if ((c == QUOTE && currentChars[0] != QUOTE) || depth == skipPos) {
+ } else if ((c == Keyboard.CODE_SINGLE_QUOTE
+ && currentChars[0] != Keyboard.CODE_SINGLE_QUOTE) || depth == skipPos) {
// Skip the ' and continue deeper
word[depth] = c;
if (children != null) {
- getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
+ getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
skipPos, callback);
}
} else {
@@ -299,10 +327,16 @@ public class ExpandableDictionary extends Dictionary {
if (codeSize == inputIndex + 1) {
if (terminal) {
- if (INCLUDE_TYPED_WORD_IF_VALID
+ if (INCLUDE_TYPED_WORD_IF_VALID
|| !same(word, depth + 1, codes.getTypedWord())) {
- int finalFreq = freq * snr * addedAttenuation;
- if (skipPos < 0) finalFreq *= FULL_WORD_FREQ_MULTIPLIER;
+ final int finalFreq;
+ if (skipPos < 0) {
+ finalFreq = freq * snr * addedAttenuation
+ * FULL_WORD_SCORE_MULTIPLIER;
+ } else {
+ finalFreq = computeSkippedWordFinalFreq(freq,
+ snr * addedAttenuation, mInputLength);
+ }
callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId,
DataType.UNIGRAM);
}
@@ -313,7 +347,7 @@ public class ExpandableDictionary extends Dictionary {
skipPos, callback);
}
} else if (children != null) {
- getWordsRec(children, codes, word, depth + 1,
+ getWordsRec(children, codes, word, depth + 1,
false, snr * addedAttenuation, inputIndex + 1,
skipPos, callback);
}
@@ -333,31 +367,33 @@ public class ExpandableDictionary extends Dictionary {
/**
* Adds bigrams to the in-memory trie structure that is being used to retrieve any word
- * @param frequency frequency for this bigrams
- * @param addFrequency if true, it adds to current frequency
+ * @param frequency frequency for this bigram
+ * @param addFrequency if true, it adds to current frequency, else it overwrites the old value
* @return returns the final frequency
*/
private int addOrSetBigram(String word1, String word2, int frequency, boolean addFrequency) {
- Node firstWord = searchWord(mRoots, word1, 0, null);
+ // We don't want results to be different according to case of the looked up left hand side
+ // word. We do want however to return the correct case for the right hand side.
+ // So we want to squash the case of the left hand side, and preserve that of the right
+ // hand side word.
+ Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null);
Node secondWord = searchWord(mRoots, word2, 0, null);
- LinkedList<NextWord> bigram = firstWord.ngrams;
+ LinkedList<NextWord> bigram = firstWord.mNGrams;
if (bigram == null || bigram.size() == 0) {
- firstWord.ngrams = new LinkedList<NextWord>();
- bigram = firstWord.ngrams;
+ firstWord.mNGrams = new LinkedList<NextWord>();
+ bigram = firstWord.mNGrams;
} else {
for (NextWord nw : bigram) {
- if (nw.word == secondWord) {
+ if (nw.mWord == secondWord) {
if (addFrequency) {
- nw.frequency += frequency;
+ return nw.addFrequency(frequency);
} else {
- nw.frequency = frequency;
+ return nw.setFrequency(frequency);
}
- return nw.frequency;
}
}
}
- NextWord nw = new NextWord(secondWord, frequency);
- firstWord.ngrams.add(nw);
+ firstWord.mNGrams.add(new NextWord(secondWord, frequency));
return frequency;
}
@@ -369,31 +405,31 @@ public class ExpandableDictionary extends Dictionary {
final int wordLength = word.length();
final char c = word.charAt(depth);
// Does children have the current character?
- final int childrenLength = children.length;
+ final int childrenLength = children.mLength;
Node childNode = null;
boolean found = false;
for (int i = 0; i < childrenLength; i++) {
- childNode = children.data[i];
- if (childNode.code == c) {
+ childNode = children.mData[i];
+ if (childNode.mCode == c) {
found = true;
break;
}
}
if (!found) {
childNode = new Node();
- childNode.code = c;
- childNode.parent = parentNode;
+ childNode.mCode = c;
+ childNode.mParent = parentNode;
children.add(childNode);
}
if (wordLength == depth + 1) {
// Terminate this word
- childNode.terminal = true;
+ childNode.mTerminal = true;
return childNode;
}
- if (childNode.children == null) {
- childNode.children = new NodeArray();
+ if (childNode.mChildren == null) {
+ childNode.mChildren = new NodeArray();
}
- return searchWord(childNode.children, word, depth + 1, childNode);
+ return searchWord(childNode.mChildren, word, depth + 1, childNode);
}
// @VisibleForTesting
@@ -406,18 +442,22 @@ public class ExpandableDictionary extends Dictionary {
}
}
- private void runReverseLookUp(final CharSequence previousWord, final WordCallback callback) {
- Node prevWord = searchNode(mRoots, previousWord, 0, previousWord.length());
- if (prevWord != null && prevWord.ngrams != null) {
- reverseLookUp(prevWord.ngrams, callback);
+ private void runBigramReverseLookUp(final CharSequence previousWord,
+ final WordCallback callback) {
+ // Search for the lowercase version of the word only, because that's where bigrams
+ // store their sons.
+ Node prevWord = searchNode(mRoots, previousWord.toString().toLowerCase(), 0,
+ previousWord.length());
+ if (prevWord != null && prevWord.mNGrams != null) {
+ reverseLookUp(prevWord.mNGrams, callback);
}
}
@Override
public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback, int[] nextLettersFrequencies) {
+ final WordCallback callback) {
if (!reloadDictionaryIfRequired()) {
- runReverseLookUp(previousWord, callback);
+ runBigramReverseLookUp(previousWord, callback);
}
}
@@ -430,10 +470,14 @@ public class ExpandableDictionary extends Dictionary {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
+ //
}
}
}
+ // Local to reverseLookUp, but do not allocate each time.
+ private final char[] mLookedUpString = new char[MAX_WORD_LENGTH];
+
/**
* reverseLookUp retrieves the full word given a list of terminal nodes and adds those words
* through callback.
@@ -444,42 +488,45 @@ public class ExpandableDictionary extends Dictionary {
Node node;
int freq;
for (NextWord nextWord : terminalNodes) {
- node = nextWord.word;
- freq = nextWord.frequency;
- // TODO Not the best way to limit suggestion threshold
- if (freq >= UserBigramDictionary.SUGGEST_THRESHOLD) {
- sb.setLength(0);
- do {
- sb.insert(0, node.code);
- node = node.parent;
- } while(node != null);
-
- // TODO better way to feed char array?
- callback.addWord(sb.toString().toCharArray(), 0, sb.length(), freq, mDicTypeId,
- DataType.BIGRAM);
- }
+ node = nextWord.mWord;
+ freq = nextWord.getFrequency();
+ int index = MAX_WORD_LENGTH;
+ do {
+ --index;
+ mLookedUpString[index] = node.mCode;
+ node = node.mParent;
+ } while (node != null);
+
+ callback.addWord(mLookedUpString, index, MAX_WORD_LENGTH - index, freq, mDicTypeId,
+ DataType.BIGRAM);
}
}
/**
- * Search for the terminal node of the word
+ * Recursively search for the terminal node of the word.
+ *
+ * One iteration takes the full word to search for and the current index of the recursion.
+ *
+ * @param children the node of the trie to search under.
+ * @param word the word to search for. Only read [offset..length] so there may be trailing chars
+ * @param offset the index in {@code word} this recursion should operate on.
+ * @param length the length of the input word.
* @return Returns the terminal node of the word if the word exists
*/
private Node searchNode(final NodeArray children, final CharSequence word, final int offset,
final int length) {
- // TODO Consider combining with addWordRec
- final int count = children.length;
- char currentChar = word.charAt(offset);
+ final int count = children.mLength;
+ final char currentChar = word.charAt(offset);
for (int j = 0; j < count; j++) {
- final Node node = children.data[j];
- if (node.code == currentChar) {
+ final Node node = children.mData[j];
+ if (node.mCode == currentChar) {
if (offset == length - 1) {
- if (node.terminal) {
+ if (node.mTerminal) {
return node;
}
} else {
- if (node.children != null) {
- Node returnNode = searchNode(node.children, word, offset + 1, length);
+ if (node.mChildren != null) {
+ Node returnNode = searchNode(node.mChildren, word, offset + 1, length);
if (returnNode != null) return returnNode;
}
}
@@ -503,16 +550,17 @@ public class ExpandableDictionary extends Dictionary {
}
}
- static char toLowerCase(char c) {
+ private static char toLowerCase(char c) {
+ char baseChar = c;
if (c < BASE_CHARS.length) {
- c = BASE_CHARS[c];
+ baseChar = BASE_CHARS[c];
}
- if (c >= 'A' && c <= 'Z') {
- c = (char) (c | 32);
- } else if (c > 127) {
- c = Character.toLowerCase(c);
+ if (baseChar >= 'A' && baseChar <= 'Z') {
+ return (char)(baseChar | 32);
+ } else if (baseChar > 127) {
+ return Character.toLowerCase(baseChar);
}
- return c;
+ return baseChar;
}
/**
@@ -521,7 +569,7 @@ public class ExpandableDictionary extends Dictionary {
* if c is not a combined character, or the base character if it
* is combined.
*/
- static final char BASE_CHARS[] = {
+ private static final char BASE_CHARS[] = {
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
diff --git a/java/src/com/android/inputmethod/latin/Flag.java b/java/src/com/android/inputmethod/latin/Flag.java
new file mode 100644
index 000000000..3cb8f7e17
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/Flag.java
@@ -0,0 +1,64 @@
+/*
+ * 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.latin;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+public class Flag {
+ public final String mName;
+ public final int mResource;
+ public final int mMask;
+ public final int mSource;
+
+ static private final int SOURCE_CONFIG = 1;
+ static private final int SOURCE_EXTRAVALUE = 2;
+
+ public Flag(int resourceId, int mask) {
+ mName = null;
+ mResource = resourceId;
+ mSource = SOURCE_CONFIG;
+ mMask = mask;
+ }
+
+ public Flag(String name, int mask) {
+ mName = name;
+ mResource = 0;
+ mSource = SOURCE_EXTRAVALUE;
+ mMask = mask;
+ }
+
+ // If context/switcher are null, set all related flags in flagArray to on.
+ public static int initFlags(Flag[] flagArray, Context context, SubtypeSwitcher switcher) {
+ int flags = 0;
+ final Resources res = null == context ? null : context.getResources();
+ for (Flag entry : flagArray) {
+ switch (entry.mSource) {
+ case Flag.SOURCE_CONFIG:
+ if (res == null || res.getBoolean(entry.mResource))
+ flags |= entry.mMask;
+ break;
+ case Flag.SOURCE_EXTRAVALUE:
+ if (switcher == null ||
+ switcher.currentSubtypeContainsExtraValueKey(entry.mName))
+ flags |= entry.mMask;
+ break;
+ }
+ }
+ return flags;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java
deleted file mode 100644
index 26854399b..000000000
--- a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright (C) 2008-2009 Google Inc.
- *
- * 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.latin;
-
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.content.res.Configuration;
-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 java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Locale;
-
-public class InputLanguageSelection extends PreferenceActivity {
-
- private String mSelectedLanguages;
- private ArrayList<Loc> mAvailableLanguages = new ArrayList<Loc>();
-
- private static final String[] WHITELIST_LANGUAGES = {
- "cs", "da", "de", "en_GB", "en_US", "es", "es_US", "fr", "it", "nb", "nl", "pl", "pt",
- "ru", "tr",
- };
-
- private static boolean isWhitelisted(String lang) {
- for (String s : WHITELIST_LANGUAGES) {
- if (s.equalsIgnoreCase(lang)) {
- return true;
- }
- }
- return false;
- }
-
- private static class Loc implements Comparable<Object> {
- static Collator sCollator = Collator.getInstance();
-
- String label;
- Locale locale;
-
- public Loc(String label, Locale locale) {
- this.label = label;
- this.locale = locale;
- }
-
- @Override
- public String toString() {
- return this.label;
- }
-
- public int compareTo(Object o) {
- return sCollator.compare(this.label, ((Loc) o).label);
- }
- }
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- addPreferencesFromResource(R.xml.language_prefs);
- // Get the settings preferences
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
- mSelectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, "");
- String[] languageList = mSelectedLanguages.split(",");
- mAvailableLanguages = getUniqueLocales();
- PreferenceGroup parent = getPreferenceScreen();
- for (int i = 0; i < mAvailableLanguages.size(); i++) {
- CheckBoxPreference pref = new CheckBoxPreference(this);
- Locale locale = mAvailableLanguages.get(i).locale;
- pref.setTitle(LanguageSwitcher.toTitleCase(locale.getDisplayName(locale), locale));
- boolean checked = isLocaleIn(locale, languageList);
- pref.setChecked(checked);
- if (hasDictionary(locale)) {
- pref.setSummary(R.string.has_dictionary);
- }
- 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 boolean hasDictionary(Locale locale) {
- Resources res = getResources();
- Configuration conf = res.getConfiguration();
- Locale saveLocale = conf.locale;
- boolean haveDictionary = false;
- conf.locale = locale;
- res.updateConfiguration(conf, res.getDisplayMetrics());
-
- int[] dictionaries = LatinIME.getDictionary(res);
- BinaryDictionary bd = new BinaryDictionary(this, dictionaries, Suggest.DIC_MAIN);
-
- // Is the dictionary larger than a placeholder? Arbitrarily chose a lower limit of
- // 4000-5000 words, whereas the LARGE_DICTIONARY is about 20000+ words.
- if (bd.getSize() > Suggest.LARGE_DICTIONARY_THRESHOLD / 4) {
- haveDictionary = true;
- }
- bd.close();
- conf.locale = saveLocale;
- res.updateConfiguration(conf, res.getDisplayMetrics());
- return haveDictionary;
- }
-
- 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()) {
- Locale locale = mAvailableLanguages.get(i).locale;
- checkedLanguages += get5Code(locale) + ",";
- }
- }
- if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
- Editor editor = sp.edit();
- editor.putString(LatinIME.PREF_SELECTED_LANGUAGES, checkedLanguages);
- SharedPreferencesCompat.apply(editor);
- }
-
- ArrayList<Loc> getUniqueLocales() {
- String[] locales = getAssets().getLocales();
- Arrays.sort(locales);
- ArrayList<Loc> uniqueLocales = new ArrayList<Loc>();
-
- final int origSize = locales.length;
- Loc[] preprocess = new Loc[origSize];
- int finalSize = 0;
- for (int i = 0 ; i < origSize; i++ ) {
- String s = locales[i];
- int len = s.length();
- final Locale l;
- final String language;
- if (len == 5) {
- language = s.substring(0, 2);
- String country = s.substring(3, 5);
- l = new Locale(language, country);
- } else if (len == 2) {
- language = s;
- l = new Locale(language);
- } else {
- continue;
- }
- // Exclude languages that are not relevant to LatinIME
- if (!isWhitelisted(s)) continue;
-
- if (finalSize == 0) {
- preprocess[finalSize++] =
- new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName(l), l), l);
- } else {
- // check previous entry:
- // same lang and a country -> upgrade to full name and
- // insert ours with full name
- // diff lang -> insert ours with lang-only name
- if (preprocess[finalSize-1].locale.getLanguage().equals(
- language)) {
- preprocess[finalSize-1].label = LanguageSwitcher.toTitleCase(
- preprocess[finalSize-1].locale.getDisplayName(),
- preprocess[finalSize-1].locale);
- preprocess[finalSize++] =
- new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName(), l), l);
- } else {
- String displayName;
- if (s.equals("zz_ZZ")) {
- } else {
- displayName = LanguageSwitcher.toTitleCase(l.getDisplayName(l), l);
- preprocess[finalSize++] = new Loc(displayName, l);
- }
- }
- }
- }
- for (int i = 0; i < finalSize ; i++) {
- uniqueLocales.add(preprocess[i]);
- }
- return uniqueLocales;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/KeyDetector.java b/java/src/com/android/inputmethod/latin/KeyDetector.java
deleted file mode 100644
index 76fe1200e..000000000
--- a/java/src/com/android/inputmethod/latin/KeyDetector.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2010 Google Inc.
- *
- * 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.latin;
-
-import android.inputmethodservice.Keyboard;
-import android.inputmethodservice.Keyboard.Key;
-
-import java.util.Arrays;
-import java.util.List;
-
-abstract class KeyDetector {
- protected Keyboard mKeyboard;
-
- private Key[] mKeys;
-
- protected int mCorrectionX;
-
- protected int mCorrectionY;
-
- protected boolean mProximityCorrectOn;
-
- protected int mProximityThresholdSquare;
-
- public Key[] setKeyboard(Keyboard keyboard, float correctionX, float correctionY) {
- if (keyboard == null)
- throw new NullPointerException();
- mCorrectionX = (int)correctionX;
- mCorrectionY = (int)correctionY;
- mKeyboard = keyboard;
- List<Key> keys = mKeyboard.getKeys();
- Key[] array = keys.toArray(new Key[keys.size()]);
- mKeys = array;
- return array;
- }
-
- protected int getTouchX(int x) {
- return x + mCorrectionX;
- }
-
- protected int getTouchY(int y) {
- return y + mCorrectionY;
- }
-
- protected Key[] getKeys() {
- if (mKeys == null)
- throw new IllegalStateException("keyboard isn't set");
- // mKeyboard is guaranteed not to be null at setKeybaord() method if mKeys is not null
- return mKeys;
- }
-
- public void setProximityCorrectionEnabled(boolean enabled) {
- mProximityCorrectOn = enabled;
- }
-
- public boolean isProximityCorrectionEnabled() {
- return mProximityCorrectOn;
- }
-
- public void setProximityThreshold(int threshold) {
- mProximityThresholdSquare = threshold * threshold;
- }
-
- /**
- * Allocates array that can hold all key indices returned by {@link #getKeyIndexAndNearbyCodes}
- * method. The maximum size of the array should be computed by {@link #getMaxNearbyKeys}.
- *
- * @return Allocates and returns an array that can hold all key indices returned by
- * {@link #getKeyIndexAndNearbyCodes} method. All elements in the returned array are
- * initialized by {@link com.android.inputmethod.latin.LatinKeyboardView.NOT_A_KEY}
- * value.
- */
- public int[] newCodeArray() {
- int[] codes = new int[getMaxNearbyKeys()];
- Arrays.fill(codes, LatinKeyboardBaseView.NOT_A_KEY);
- return codes;
- }
-
- /**
- * Computes maximum size of the array that can contain all nearby key indices returned by
- * {@link #getKeyIndexAndNearbyCodes}.
- *
- * @return Returns maximum size of the array that can contain all nearby key indices returned
- * by {@link #getKeyIndexAndNearbyCodes}.
- */
- abstract protected int getMaxNearbyKeys();
-
- /**
- * Finds all possible nearby key indices around a touch event point and returns the nearest key
- * index. The algorithm to determine the nearby keys depends on the threshold set by
- * {@link #setProximityThreshold(int)} and the mode set by
- * {@link #setProximityCorrectionEnabled(boolean)}.
- *
- * @param x The x-coordinate of a touch point
- * @param y The y-coordinate of a touch point
- * @param allKeys All nearby key indices are returned in this array
- * @return The nearest key index
- */
- abstract public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys);
-}
diff --git a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java
deleted file mode 100644
index 0db204ed8..000000000
--- a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java
+++ /dev/null
@@ -1,606 +0,0 @@
-/*
- * 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.latin;
-
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.preference.PreferenceManager;
-import android.view.InflateException;
-
-import java.lang.ref.SoftReference;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Locale;
-
-public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener {
-
- public static final int MODE_NONE = 0;
- public static final int MODE_TEXT = 1;
- public static final int MODE_SYMBOLS = 2;
- public static final int MODE_PHONE = 3;
- public static final int MODE_URL = 4;
- public static final int MODE_EMAIL = 5;
- public static final int MODE_IM = 6;
- public static final int MODE_WEB = 7;
-
- // Main keyboard layouts without the settings key
- public static final int KEYBOARDMODE_NORMAL = R.id.mode_normal;
- public static final int KEYBOARDMODE_URL = R.id.mode_url;
- public static final int KEYBOARDMODE_EMAIL = R.id.mode_email;
- public static final int KEYBOARDMODE_IM = R.id.mode_im;
- public static final int KEYBOARDMODE_WEB = R.id.mode_webentry;
- // Main keyboard layouts with the settings key
- public static final int KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY =
- R.id.mode_normal_with_settings_key;
- public static final int KEYBOARDMODE_URL_WITH_SETTINGS_KEY =
- R.id.mode_url_with_settings_key;
- public static final int KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY =
- R.id.mode_email_with_settings_key;
- public static final int KEYBOARDMODE_IM_WITH_SETTINGS_KEY =
- R.id.mode_im_with_settings_key;
- public static final int KEYBOARDMODE_WEB_WITH_SETTINGS_KEY =
- R.id.mode_webentry_with_settings_key;
-
- // Symbols keyboard layout without the settings key
- public static final int KEYBOARDMODE_SYMBOLS = R.id.mode_symbols;
- // Symbols keyboard layout with the settings key
- public static final int KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY =
- R.id.mode_symbols_with_settings_key;
-
- public static final String DEFAULT_LAYOUT_ID = "4";
- public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20100902";
- private static final int[] THEMES = new int [] {
- R.layout.input_basic, R.layout.input_basic_highcontrast, R.layout.input_stone_normal,
- R.layout.input_stone_bold, R.layout.input_gingerbread};
-
- // Ids for each characters' color in the keyboard
- private static final int CHAR_THEME_COLOR_WHITE = 0;
- private static final int CHAR_THEME_COLOR_BLACK = 1;
-
- // Tables which contains resource ids for each character theme color
- private static final int[] KBD_PHONE = new int[] {R.xml.kbd_phone, R.xml.kbd_phone_black};
- private static final int[] KBD_PHONE_SYMBOLS = new int[] {
- R.xml.kbd_phone_symbols, R.xml.kbd_phone_symbols_black};
- private static final int[] KBD_SYMBOLS = new int[] {
- R.xml.kbd_symbols, R.xml.kbd_symbols_black};
- private static final int[] KBD_SYMBOLS_SHIFT = new int[] {
- R.xml.kbd_symbols_shift, R.xml.kbd_symbols_shift_black};
- private static final int[] KBD_QWERTY = new int[] {R.xml.kbd_qwerty, R.xml.kbd_qwerty_black};
-
- private LatinKeyboardView mInputView;
- private static final int[] ALPHABET_MODES = {
- KEYBOARDMODE_NORMAL,
- KEYBOARDMODE_URL,
- KEYBOARDMODE_EMAIL,
- KEYBOARDMODE_IM,
- KEYBOARDMODE_WEB,
- KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY,
- KEYBOARDMODE_URL_WITH_SETTINGS_KEY,
- KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY,
- KEYBOARDMODE_IM_WITH_SETTINGS_KEY,
- KEYBOARDMODE_WEB_WITH_SETTINGS_KEY };
-
- private LatinIME mInputMethodService;
-
- private KeyboardId mSymbolsId;
- private KeyboardId mSymbolsShiftedId;
-
- private KeyboardId mCurrentId;
- private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboards =
- new HashMap<KeyboardId, SoftReference<LatinKeyboard>>();
-
- private int mMode = MODE_NONE; /** One of the MODE_XXX values */
- private int mImeOptions;
- private boolean mIsSymbols;
- /** mIsAutoCompletionActive indicates that auto completed word will be input instead of
- * what user actually typed. */
- private boolean mIsAutoCompletionActive;
- private boolean mHasVoice;
- private boolean mVoiceOnPrimary;
- private boolean mPreferSymbols;
-
- private static final int AUTO_MODE_SWITCH_STATE_ALPHA = 0;
- private static final int AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN = 1;
- private static final int AUTO_MODE_SWITCH_STATE_SYMBOL = 2;
- // The following states are used only on the distinct multi-touch panel devices.
- private static final int AUTO_MODE_SWITCH_STATE_MOMENTARY = 3;
- private static final int AUTO_MODE_SWITCH_STATE_CHORDING = 4;
- private int mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA;
-
- // Indicates whether or not we have the settings key
- private boolean mHasSettingsKey;
- private static final int SETTINGS_KEY_MODE_AUTO = R.string.settings_key_mode_auto;
- private static final int SETTINGS_KEY_MODE_ALWAYS_SHOW = R.string.settings_key_mode_always_show;
- // NOTE: No need to have SETTINGS_KEY_MODE_ALWAYS_HIDE here because it's not being referred to
- // in the source code now.
- // Default is SETTINGS_KEY_MODE_AUTO.
- private static final int DEFAULT_SETTINGS_KEY_MODE = SETTINGS_KEY_MODE_AUTO;
-
- private int mLastDisplayWidth;
- private LanguageSwitcher mLanguageSwitcher;
- private Locale mInputLocale;
-
- private int mLayoutId;
-
- private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
-
- public static KeyboardSwitcher getInstance() {
- return sInstance;
- }
-
- private KeyboardSwitcher() {
- // Intentional empty constructor for singleton.
- }
-
- public static void init(LatinIME ims) {
- sInstance.mInputMethodService = ims;
-
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ims);
- sInstance.mLayoutId = Integer.valueOf(
- prefs.getString(PREF_KEYBOARD_LAYOUT, DEFAULT_LAYOUT_ID));
- sInstance.updateSettingsKeyState(prefs);
- prefs.registerOnSharedPreferenceChangeListener(sInstance);
-
- sInstance.mSymbolsId = sInstance.makeSymbolsId(false);
- sInstance.mSymbolsShiftedId = sInstance.makeSymbolsShiftedId(false);
- }
-
- /**
- * Sets the input locale, when there are multiple locales for input.
- * If no locale switching is required, then the locale should be set to null.
- * @param locale the current input locale, or null for default locale with no locale
- * button.
- */
- public void setLanguageSwitcher(LanguageSwitcher languageSwitcher) {
- mLanguageSwitcher = languageSwitcher;
- mInputLocale = mLanguageSwitcher.getInputLocale();
- }
-
- private KeyboardId makeSymbolsId(boolean hasVoice) {
- return new KeyboardId(KBD_SYMBOLS[getCharColorId()], mHasSettingsKey ?
- KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS,
- false, hasVoice);
- }
-
- private KeyboardId makeSymbolsShiftedId(boolean hasVoice) {
- return new KeyboardId(KBD_SYMBOLS_SHIFT[getCharColorId()], mHasSettingsKey ?
- KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS,
- false, hasVoice);
- }
-
- public void makeKeyboards(boolean forceCreate) {
- mSymbolsId = makeSymbolsId(mHasVoice && !mVoiceOnPrimary);
- mSymbolsShiftedId = makeSymbolsShiftedId(mHasVoice && !mVoiceOnPrimary);
-
- if (forceCreate) mKeyboards.clear();
- // Configuration change is coming after the keyboard gets recreated. So don't rely on that.
- // If keyboards have already been made, check if we have a screen width change and
- // create the keyboard layouts again at the correct orientation
- int displayWidth = mInputMethodService.getMaxWidth();
- if (displayWidth == mLastDisplayWidth) return;
- mLastDisplayWidth = displayWidth;
- if (!forceCreate) mKeyboards.clear();
- }
-
- /**
- * Represents the parameters necessary to construct a new LatinKeyboard,
- * which also serve as a unique identifier for each keyboard type.
- */
- private static class KeyboardId {
- // TODO: should have locale and portrait/landscape orientation?
- public final int mXml;
- public final int mKeyboardMode; /** A KEYBOARDMODE_XXX value */
- public final boolean mEnableShiftLock;
- public final boolean mHasVoice;
-
- private final int mHashCode;
-
- public KeyboardId(int xml, int mode, boolean enableShiftLock, boolean hasVoice) {
- this.mXml = xml;
- this.mKeyboardMode = mode;
- this.mEnableShiftLock = enableShiftLock;
- this.mHasVoice = hasVoice;
-
- this.mHashCode = Arrays.hashCode(new Object[] {
- xml, mode, enableShiftLock, hasVoice
- });
- }
-
- public KeyboardId(int xml, boolean hasVoice) {
- this(xml, 0, false, hasVoice);
- }
-
- @Override
- public boolean equals(Object other) {
- return other instanceof KeyboardId && equals((KeyboardId) other);
- }
-
- private boolean equals(KeyboardId other) {
- return other.mXml == this.mXml
- && other.mKeyboardMode == this.mKeyboardMode
- && other.mEnableShiftLock == this.mEnableShiftLock
- && other.mHasVoice == this.mHasVoice;
- }
-
- @Override
- public int hashCode() {
- return mHashCode;
- }
- }
-
- public void setVoiceMode(boolean enableVoice, boolean voiceOnPrimary) {
- if (enableVoice != mHasVoice || voiceOnPrimary != mVoiceOnPrimary) {
- mKeyboards.clear();
- }
- mHasVoice = enableVoice;
- mVoiceOnPrimary = voiceOnPrimary;
- setKeyboardMode(mMode, mImeOptions, mHasVoice, mIsSymbols);
- }
-
- private boolean hasVoiceButton(boolean isSymbols) {
- return mHasVoice && (isSymbols != mVoiceOnPrimary);
- }
-
- public void setKeyboardMode(int mode, int imeOptions, boolean enableVoice) {
- mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA;
- mPreferSymbols = mode == MODE_SYMBOLS;
- if (mode == MODE_SYMBOLS) {
- mode = MODE_TEXT;
- }
- try {
- setKeyboardMode(mode, imeOptions, enableVoice, mPreferSymbols);
- } catch (RuntimeException e) {
- LatinImeLogger.logOnException(mode + "," + imeOptions + "," + mPreferSymbols, e);
- }
- }
-
- private void setKeyboardMode(int mode, int imeOptions, boolean enableVoice, boolean isSymbols) {
- if (mInputView == null) return;
- mMode = mode;
- mImeOptions = imeOptions;
- if (enableVoice != mHasVoice) {
- // TODO clean up this unnecessary recursive call.
- setVoiceMode(enableVoice, mVoiceOnPrimary);
- }
- mIsSymbols = isSymbols;
-
- mInputView.setPreviewEnabled(mInputMethodService.getPopupOn());
- KeyboardId id = getKeyboardId(mode, imeOptions, isSymbols);
- LatinKeyboard keyboard = null;
- keyboard = getKeyboard(id);
-
- if (mode == MODE_PHONE) {
- mInputView.setPhoneKeyboard(keyboard);
- }
-
- mCurrentId = id;
- mInputView.setKeyboard(keyboard);
- keyboard.setShifted(false);
- keyboard.setShiftLocked(keyboard.isShiftLocked());
- keyboard.setImeOptions(mInputMethodService.getResources(), mMode, imeOptions);
- keyboard.setColorOfSymbolIcons(mIsAutoCompletionActive, isBlackSym());
- // Update the settings key state because number of enabled IMEs could have been changed
- updateSettingsKeyState(PreferenceManager.getDefaultSharedPreferences(mInputMethodService));
- }
-
- private LatinKeyboard getKeyboard(KeyboardId id) {
- SoftReference<LatinKeyboard> ref = mKeyboards.get(id);
- LatinKeyboard keyboard = (ref == null) ? null : ref.get();
- if (keyboard == null) {
- Resources orig = mInputMethodService.getResources();
- Configuration conf = orig.getConfiguration();
- Locale saveLocale = conf.locale;
- conf.locale = mInputLocale;
- orig.updateConfiguration(conf, null);
- keyboard = new LatinKeyboard(mInputMethodService, id.mXml, id.mKeyboardMode);
- keyboard.setVoiceMode(hasVoiceButton(id.mXml == R.xml.kbd_symbols
- || id.mXml == R.xml.kbd_symbols_black), mHasVoice);
- keyboard.setLanguageSwitcher(mLanguageSwitcher, mIsAutoCompletionActive, isBlackSym());
-
- if (id.mEnableShiftLock) {
- keyboard.enableShiftLock();
- }
- mKeyboards.put(id, new SoftReference<LatinKeyboard>(keyboard));
-
- conf.locale = saveLocale;
- orig.updateConfiguration(conf, null);
- }
- return keyboard;
- }
-
- private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) {
- boolean hasVoice = hasVoiceButton(isSymbols);
- int charColorId = getCharColorId();
- // TODO: generalize for any KeyboardId
- int keyboardRowsResId = KBD_QWERTY[charColorId];
- if (isSymbols) {
- if (mode == MODE_PHONE) {
- return new KeyboardId(KBD_PHONE_SYMBOLS[charColorId], hasVoice);
- } else {
- return new KeyboardId(KBD_SYMBOLS[charColorId], mHasSettingsKey ?
- KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS,
- false, hasVoice);
- }
- }
- switch (mode) {
- case MODE_NONE:
- LatinImeLogger.logOnWarning(
- "getKeyboardId:" + mode + "," + imeOptions + "," + isSymbols);
- /* fall through */
- case MODE_TEXT:
- return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
- KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY : KEYBOARDMODE_NORMAL,
- true, hasVoice);
- case MODE_SYMBOLS:
- return new KeyboardId(KBD_SYMBOLS[charColorId], mHasSettingsKey ?
- KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS,
- false, hasVoice);
- case MODE_PHONE:
- return new KeyboardId(KBD_PHONE[charColorId], hasVoice);
- case MODE_URL:
- return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
- KEYBOARDMODE_URL_WITH_SETTINGS_KEY : KEYBOARDMODE_URL, true, hasVoice);
- case MODE_EMAIL:
- return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
- KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY : KEYBOARDMODE_EMAIL, true, hasVoice);
- case MODE_IM:
- return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
- KEYBOARDMODE_IM_WITH_SETTINGS_KEY : KEYBOARDMODE_IM, true, hasVoice);
- case MODE_WEB:
- return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
- KEYBOARDMODE_WEB_WITH_SETTINGS_KEY : KEYBOARDMODE_WEB, true, hasVoice);
- }
- return null;
- }
-
- public int getKeyboardMode() {
- return mMode;
- }
-
- public boolean isAlphabetMode() {
- if (mCurrentId == null) {
- return false;
- }
- int currentMode = mCurrentId.mKeyboardMode;
- for (Integer mode : ALPHABET_MODES) {
- if (currentMode == mode) {
- return true;
- }
- }
- return false;
- }
-
- public void setShifted(boolean shifted) {
- if (mInputView != null) {
- mInputView.setShifted(shifted);
- }
- }
-
- public void setShiftLocked(boolean shiftLocked) {
- if (mInputView != null) {
- mInputView.setShiftLocked(shiftLocked);
- }
- }
-
- public void toggleShift() {
- if (isAlphabetMode())
- return;
- if (mCurrentId.equals(mSymbolsId) || !mCurrentId.equals(mSymbolsShiftedId)) {
- LatinKeyboard symbolsShiftedKeyboard = getKeyboard(mSymbolsShiftedId);
- mCurrentId = mSymbolsShiftedId;
- mInputView.setKeyboard(symbolsShiftedKeyboard);
- // Symbol shifted keyboard has an ALT key that has a caps lock style indicator. To
- // enable the indicator, we need to call enableShiftLock() and setShiftLocked(true).
- // Thus we can keep the ALT key's Key.on value true while LatinKey.onRelease() is
- // called.
- symbolsShiftedKeyboard.enableShiftLock();
- symbolsShiftedKeyboard.setShiftLocked(true);
- symbolsShiftedKeyboard.setImeOptions(mInputMethodService.getResources(),
- mMode, mImeOptions);
- } else {
- LatinKeyboard symbolsKeyboard = getKeyboard(mSymbolsId);
- mCurrentId = mSymbolsId;
- mInputView.setKeyboard(symbolsKeyboard);
- // Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the
- // indicator, we need to call enableShiftLock() and setShiftLocked(false).
- symbolsKeyboard.enableShiftLock();
- symbolsKeyboard.setShifted(false);
- symbolsKeyboard.setImeOptions(mInputMethodService.getResources(), mMode, mImeOptions);
- }
- }
-
- public void onCancelInput() {
- // Snap back to the previous keyboard mode if the user cancels sliding input.
- if (mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_MOMENTARY && getPointerCount() == 1)
- mInputMethodService.changeKeyboardMode();
- }
-
- public void toggleSymbols() {
- setKeyboardMode(mMode, mImeOptions, mHasVoice, !mIsSymbols);
- if (mIsSymbols && !mPreferSymbols) {
- mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN;
- } else {
- mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA;
- }
- }
-
- public boolean hasDistinctMultitouch() {
- return mInputView != null && mInputView.hasDistinctMultitouch();
- }
-
- public void setAutoModeSwitchStateMomentary() {
- mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_MOMENTARY;
- }
-
- public boolean isInMomentaryAutoModeSwitchState() {
- return mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_MOMENTARY;
- }
-
- public boolean isInChordingAutoModeSwitchState() {
- return mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_CHORDING;
- }
-
- public boolean isVibrateAndSoundFeedbackRequired() {
- return mInputView != null && !mInputView.isInSlidingKeyInput();
- }
-
- private int getPointerCount() {
- return mInputView == null ? 0 : mInputView.getPointerCount();
- }
-
- /**
- * Updates state machine to figure out when to automatically snap back to the previous mode.
- */
- public void onKey(int key) {
- // Switch back to alpha mode if user types one or more non-space/enter characters
- // followed by a space/enter
- switch (mAutoModeSwitchState) {
- case AUTO_MODE_SWITCH_STATE_MOMENTARY:
- // Only distinct multi touch devices can be in this state.
- // On non-distinct multi touch devices, mode change key is handled by {@link onKey},
- // not by {@link onPress} and {@link onRelease}. So, on such devices,
- // {@link mAutoModeSwitchState} starts from {@link AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN},
- // or {@link AUTO_MODE_SWITCH_STATE_ALPHA}, not from
- // {@link AUTO_MODE_SWITCH_STATE_MOMENTARY}.
- if (key == LatinKeyboard.KEYCODE_MODE_CHANGE) {
- // Detected only the mode change key has been pressed, and then released.
- if (mIsSymbols) {
- mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN;
- } else {
- mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA;
- }
- } else if (getPointerCount() == 1) {
- // Snap back to the previous keyboard mode if the user pressed the mode change key
- // and slid to other key, then released the finger.
- // If the user cancels the sliding input, snapping back to the previous keyboard
- // mode is handled by {@link #onCancelInput}.
- mInputMethodService.changeKeyboardMode();
- } else {
- // Chording input is being started. The keyboard mode will be snapped back to the
- // previous mode in {@link onReleaseSymbol} when the mode change key is released.
- mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_CHORDING;
- }
- break;
- case AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN:
- if (key != LatinIME.KEYCODE_SPACE && key != LatinIME.KEYCODE_ENTER && key >= 0) {
- mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL;
- }
- break;
- case AUTO_MODE_SWITCH_STATE_SYMBOL:
- // Snap back to alpha keyboard mode if user types one or more non-space/enter
- // characters followed by a space/enter.
- if (key == LatinIME.KEYCODE_ENTER || key == LatinIME.KEYCODE_SPACE) {
- mInputMethodService.changeKeyboardMode();
- }
- break;
- }
- }
-
- public LatinKeyboardView getInputView() {
- return mInputView;
- }
-
- public void recreateInputView() {
- changeLatinKeyboardView(mLayoutId, true);
- }
-
- private void changeLatinKeyboardView(int newLayout, boolean forceReset) {
- if (mLayoutId != newLayout || mInputView == null || forceReset) {
- if (mInputView != null) {
- mInputView.closing();
- }
- if (THEMES.length <= newLayout) {
- newLayout = Integer.valueOf(DEFAULT_LAYOUT_ID);
- }
-
- LatinIMEUtil.GCUtils.getInstance().reset();
- boolean tryGC = true;
- for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
- try {
- mInputView = (LatinKeyboardView) mInputMethodService.getLayoutInflater(
- ).inflate(THEMES[newLayout], null);
- tryGC = false;
- } catch (OutOfMemoryError e) {
- tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(
- mLayoutId + "," + newLayout, e);
- } catch (InflateException e) {
- tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(
- mLayoutId + "," + newLayout, e);
- }
- }
- mInputView.setOnKeyboardActionListener(mInputMethodService);
- mLayoutId = newLayout;
- }
- mInputMethodService.mHandler.post(new Runnable() {
- public void run() {
- if (mInputView != null) {
- mInputMethodService.setInputView(mInputView);
- }
- mInputMethodService.updateInputViewShown();
- }});
- }
-
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (PREF_KEYBOARD_LAYOUT.equals(key)) {
- changeLatinKeyboardView(
- Integer.valueOf(sharedPreferences.getString(key, DEFAULT_LAYOUT_ID)), false);
- } else if (LatinIMESettings.PREF_SETTINGS_KEY.equals(key)) {
- updateSettingsKeyState(sharedPreferences);
- recreateInputView();
- }
- }
-
- public boolean isBlackSym () {
- if (mInputView != null && mInputView.getSymbolColorScheme() == 1) {
- return true;
- }
- return false;
- }
-
- private int getCharColorId () {
- if (isBlackSym()) {
- return CHAR_THEME_COLOR_BLACK;
- } else {
- return CHAR_THEME_COLOR_WHITE;
- }
- }
-
- public void onAutoCompletionStateChanged(boolean isAutoCompletion) {
- if (isAutoCompletion != mIsAutoCompletionActive) {
- LatinKeyboardView keyboardView = getInputView();
- mIsAutoCompletionActive = isAutoCompletion;
- keyboardView.invalidateKey(((LatinKeyboard) keyboardView.getKeyboard())
- .onAutoCompletionStateChanged(isAutoCompletion));
- }
- }
-
- private void updateSettingsKeyState(SharedPreferences prefs) {
- Resources resources = mInputMethodService.getResources();
- final String settingsKeyMode = prefs.getString(LatinIMESettings.PREF_SETTINGS_KEY,
- resources.getString(DEFAULT_SETTINGS_KEY_MODE));
- // We show the settings key when 1) SETTINGS_KEY_MODE_ALWAYS_SHOW or
- // 2) SETTINGS_KEY_MODE_AUTO and there are two or more enabled IMEs on the system
- if (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW))
- || (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_AUTO))
- && LatinIMEUtil.hasMultipleEnabledIMEs(mInputMethodService))) {
- mHasSettingsKey = true;
- } else {
- mHasSettingsKey = false;
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 4f3d3ba9f..9c6465dd2 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -25,18 +25,17 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.content.res.XmlResourceParser;
import android.inputmethodservice.InputMethodService;
-import android.inputmethodservice.Keyboard;
import android.media.AudioManager;
+import android.net.ConnectivityManager;
import android.os.Debug;
import android.os.Handler;
+import android.os.IBinder;
import android.os.Message;
import android.os.SystemClock;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
-import android.speech.SpeechRecognizer;
-import android.text.ClipboardManager;
+import android.text.InputType;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -44,7 +43,6 @@ import android.util.PrintWriterPrinter;
import android.util.Printer;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
@@ -53,441 +51,422 @@ import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.LinearLayout;
-import com.android.inputmethod.latin.LatinIMEUtil.RingCharBuffer;
-import com.android.inputmethod.voice.FieldContext;
-import com.android.inputmethod.voice.SettingsUtil;
-import com.android.inputmethod.voice.VoiceInput;
-
-import org.xmlpull.v1.XmlPullParserException;
+import com.android.inputmethod.accessibility.AccessibilityUtils;
+import com.android.inputmethod.compat.CompatUtils;
+import com.android.inputmethod.compat.EditorInfoCompatUtils;
+import com.android.inputmethod.compat.InputConnectionCompatUtils;
+import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
+import com.android.inputmethod.compat.InputTypeCompatUtils;
+import com.android.inputmethod.compat.SuggestionSpanUtils;
+import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
+import com.android.inputmethod.deprecated.VoiceProxy;
+import com.android.inputmethod.deprecated.recorrection.Recorrection;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardActionListener;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.keyboard.LatinKeyboard;
+import com.android.inputmethod.keyboard.LatinKeyboardView;
import java.io.FileDescriptor;
-import java.io.IOException;
import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
import java.util.Locale;
-import java.util.Map;
/**
* Input method implementation for Qwerty'ish keyboard.
*/
-public class LatinIME extends InputMethodService
- implements LatinKeyboardBaseView.OnKeyboardActionListener,
- VoiceInput.UiListener,
- SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = "LatinIME";
+public class LatinIME extends InputMethodServiceCompatWrapper implements KeyboardActionListener,
+ CandidateView.Listener {
+ private static final String TAG = LatinIME.class.getSimpleName();
private static final boolean PERF_DEBUG = false;
- static final boolean DEBUG = false;
- static final boolean TRACE = false;
- static final boolean VOICE_INSTALLED = true;
- static final boolean ENABLE_VOICE_BUTTON = true;
-
- private static final String PREF_VIBRATE_ON = "vibrate_on";
- private static final String PREF_SOUND_ON = "sound_on";
- private static final String PREF_POPUP_ON = "popup_on";
- private static final String PREF_AUTO_CAP = "auto_cap";
- private static final String PREF_QUICK_FIXES = "quick_fixes";
- private static final String PREF_SHOW_SUGGESTIONS = "show_suggestions";
- private static final String PREF_AUTO_COMPLETE = "auto_complete";
- //private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion";
- 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";
-
- // A list of locales which are supported by default for voice input, unless we get a
- // different list from Gservices.
- public 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 ";
-
- // The private IME option used to indicate that no microphone should be shown for a
- // given text field. For instance this is specified by the search dialog when the
- // dialog is already showing a voice search button.
- private static final String IME_OPTION_NO_MICROPHONE = "nm";
-
- public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
- public static final String PREF_INPUT_LANGUAGE = "input_language";
- private static final String PREF_RECORRECTION_ENABLED = "recorrection_enabled";
-
- private static final int MSG_UPDATE_SUGGESTIONS = 0;
- private static final int MSG_START_TUTORIAL = 1;
- private static final int MSG_UPDATE_SHIFT_STATE = 2;
- private static final int MSG_VOICE_RESULTS = 3;
- private static final int MSG_UPDATE_OLD_SUGGESTIONS = 4;
+ private static final boolean TRACE = false;
+ private static boolean DEBUG;
+
+ /**
+ * The private IME option used to indicate that no microphone should be
+ * shown for a given text field. For instance, this is specified by the
+ * search dialog when the dialog is already showing a voice search button.
+ *
+ * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed.
+ */
+ @SuppressWarnings("dep-ann")
+ public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm";
+
+ /**
+ * The private IME option used to indicate that no microphone should be
+ * shown for a given text field. For instance, this is specified by the
+ * search dialog when the dialog is already showing a voice search button.
+ */
+ public static final String IME_OPTION_NO_MICROPHONE = "noMicrophoneKey";
+
+ /**
+ * The private IME option used to indicate that no settings key should be
+ * shown for a given text field.
+ */
+ public static final String IME_OPTION_NO_SETTINGS_KEY = "noSettingsKey";
+
+ private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
// How many continuous deletes at which to start deleting at a higher speed.
private static final int DELETE_ACCELERATE_AT = 20;
// Key events coming any faster than this are long-presses.
private static final int QUICK_PRESS = 200;
- static final int KEYCODE_ENTER = '\n';
- static final int KEYCODE_SPACE = ' ';
- static final int KEYCODE_PERIOD = '.';
+ /**
+ * The name of the scheme used by the Package Manager to warn of a new package installation,
+ * replacement or removal.
+ */
+ private static final String SCHEME_PACKAGE = "package";
+
+ private int mSuggestionVisibility;
+ private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
+ = R.string.prefs_suggestion_visibility_show_value;
+ private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
+ = R.string.prefs_suggestion_visibility_show_only_portrait_value;
+ private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
+ = R.string.prefs_suggestion_visibility_hide_value;
+
+ private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
+ SUGGESTION_VISIBILILTY_SHOW_VALUE,
+ SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
+ SUGGESTION_VISIBILILTY_HIDE_VALUE
+ };
- // Contextual menu positions
- private static final int POS_METHOD = 0;
- private static final int POS_SETTINGS = 1;
+ private Settings.Values mSettingsValues;
- //private LatinKeyboardView mInputView;
- private LinearLayout mCandidateViewContainer;
+ private View mCandidateViewContainer;
+ private int mCandidateStripHeight;
private CandidateView mCandidateView;
private Suggest mSuggest;
- private CompletionInfo[] mCompletions;
+ private CompletionInfo[] mApplicationSpecifiedCompletions;
private AlertDialog mOptionsDialog;
- private AlertDialog mVoiceWarningDialog;
- /* package */ KeyboardSwitcher mKeyboardSwitcher;
+ private InputMethodManagerCompatWrapper mImm;
+ private Resources mResources;
+ private SharedPreferences mPrefs;
+ private String mInputMethodId;
+ private KeyboardSwitcher mKeyboardSwitcher;
+ private SubtypeSwitcher mSubtypeSwitcher;
+ private VoiceProxy mVoiceProxy;
+ private Recorrection mRecorrection;
private UserDictionary mUserDictionary;
private UserBigramDictionary mUserBigramDictionary;
- private ContactsDictionary mContactsDictionary;
private AutoDictionary mAutoDictionary;
- private Hints mHints;
-
- private Resources mResources;
-
- private String mInputLocale;
- private String mSystemLocale;
- private LanguageSwitcher mLanguageSwitcher;
+ // TODO: Create an inner class to group options and pseudo-options to improve readability.
+ // These variables are initialized according to the {@link EditorInfo#inputType}.
+ private boolean mShouldInsertMagicSpace;
+ private boolean mInputTypeNoAutoCorrect;
+ private boolean mIsSettingsSuggestionStripOn;
+ private boolean mApplicationSpecifiedCompletionOn;
- private StringBuilder mComposing = new StringBuilder();
+ private final StringBuilder mComposing = new StringBuilder();
private WordComposer mWord = new WordComposer();
- private int mCommittedLength;
- private boolean mPredicting;
- private boolean mRecognizing;
- private boolean mAfterVoiceInput;
- private boolean mImmediatelyAfterVoiceInput;
- private boolean mShowingVoiceSuggestions;
- private boolean mVoiceInputHighlighted;
- private boolean mEnableVoiceButton;
private CharSequence mBestWord;
- private boolean mPredictionOn;
- private boolean mCompletionOn;
+ private boolean mHasUncommittedTypedChars;
private boolean mHasDictionary;
- private boolean mAutoSpace;
- private boolean mJustAddedAutoSpace;
- private boolean mAutoCorrectEnabled;
- private boolean mReCorrectionEnabled;
- // Bigram Suggestion is disabled in this version.
- private final boolean mBigramSuggestionEnabled = false;
- private boolean mAutoCorrectOn;
- // TODO move this state variable outside LatinIME
- private boolean mCapsLock;
- private boolean mPasswordText;
- private boolean mVibrateOn;
- private boolean mSoundOn;
- private boolean mPopupOn;
- private boolean mAutoCap;
- private boolean mQuickFixes;
- private boolean mHasUsedVoiceInput;
- private boolean mHasUsedVoiceInputUnsupportedLocale;
- private boolean mLocaleSupportedForVoiceInput;
- private boolean mShowSuggestions;
- private boolean mIsShowingHint;
- private int mCorrectionMode;
- private boolean mEnableVoice = true;
- private boolean mVoiceOnPrimary;
- private int mOrientation;
- private List<CharSequence> mSuggestPuncList;
+ // Magic space: a space that should disappear on space/apostrophe insertion, move after the
+ // punctuation on punctuation insertion, and become a real space on alpha char insertion.
+ private boolean mJustAddedMagicSpace; // This indicates whether the last char is a magic space.
+ // This indicates whether the last keypress resulted in processing of double space replacement
+ // with period-space.
+ private boolean mJustReplacedDoubleSpace;
+
+ private int mCorrectionMode;
+ private int mCommittedLength;
+ private int mOrientation;
// Keep track of the last selection range to decide if we need to show word alternatives
- private int mLastSelectionStart;
- private int mLastSelectionEnd;
-
- // Input type is such that we should not auto-correct
- private boolean mInputTypeNoAutoCorrect;
+ private int mLastSelectionStart;
+ private int mLastSelectionEnd;
- // Indicates whether the suggestion strip is to be on in landscape
- private boolean mJustAccepted;
- private CharSequence mJustRevertedSeparator;
+ // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
+ // "expect" it, it means the user actually moved the cursor.
+ private boolean mExpectingUpdateSelection;
private int mDeleteCount;
private long mLastKeyTime;
- // Modifier keys state
- private ModifierKeyState mShiftKeyState = new ModifierKeyState();
- private ModifierKeyState mSymbolKeyState = new ModifierKeyState();
-
- private Tutorial mTutorial;
-
private AudioManager mAudioManager;
// Align sound effect volume on music volume
- private final float FX_VOLUME = -1.0f;
- private boolean mSilentMode;
-
- /* package */ String mWordSeparators;
- private String mSentenceSeparators;
- private String mSuggestPuncs;
- private VoiceInput mVoiceInput;
- private VoiceResults mVoiceResults = new VoiceResults();
+ private static final float FX_VOLUME = -1.0f;
+ private boolean mSilentModeOn; // System-wide current configuration
+
+ // TODO: Move this flag to VoiceProxy
private boolean mConfigurationChanging;
+ // Object for reacting to adding/removing a dictionary pack.
+ private BroadcastReceiver mDictionaryPackInstallReceiver =
+ new DictionaryPackInstallBroadcastReceiver(this);
+
// Keeps track of most recently inserted text (multi-character key) for reverting
private CharSequence mEnteredText;
- private boolean mRefreshKeyboardRequired;
- // For each word, a list of potential replacements, usually from voice.
- private Map<String, List<CharSequence>> mWordToSuggestions =
- new HashMap<String, List<CharSequence>>();
- private ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>();
+ public final UIHandler mHandler = new UIHandler();
- private class VoiceResults {
- List<String> candidates;
- Map<String, List<CharSequence>> alternatives;
- }
-
- public abstract static class WordAlternatives {
- protected CharSequence mChosenWord;
+ public class UIHandler extends Handler {
+ private static final int MSG_UPDATE_SUGGESTIONS = 0;
+ private static final int MSG_UPDATE_OLD_SUGGESTIONS = 1;
+ private static final int MSG_UPDATE_SHIFT_STATE = 2;
+ private static final int MSG_VOICE_RESULTS = 3;
+ private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 4;
+ private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 5;
+ private static final int MSG_SPACE_TYPED = 6;
+ private static final int MSG_SET_BIGRAM_PREDICTIONS = 7;
- public WordAlternatives() {
- // Nothing
+ @Override
+ public void handleMessage(Message msg) {
+ final KeyboardSwitcher switcher = mKeyboardSwitcher;
+ final LatinKeyboardView inputView = switcher.getKeyboardView();
+ switch (msg.what) {
+ case MSG_UPDATE_SUGGESTIONS:
+ updateSuggestions();
+ break;
+ case MSG_UPDATE_OLD_SUGGESTIONS:
+ mRecorrection.fetchAndDisplayRecorrectionSuggestions(mVoiceProxy, mCandidateView,
+ mSuggest, mKeyboardSwitcher, mWord, mHasUncommittedTypedChars,
+ mLastSelectionStart, mLastSelectionEnd, mSettingsValues.mWordSeparators);
+ break;
+ case MSG_UPDATE_SHIFT_STATE:
+ switcher.updateShiftState();
+ break;
+ case MSG_SET_BIGRAM_PREDICTIONS:
+ updateBigramPredictions();
+ break;
+ case MSG_VOICE_RESULTS:
+ mVoiceProxy.handleVoiceResults(preferCapitalization()
+ || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked()));
+ break;
+ case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR:
+ if (inputView != null) {
+ inputView.setSpacebarTextFadeFactor(
+ (1.0f + mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar) / 2,
+ (LatinKeyboard)msg.obj);
+ }
+ sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj),
+ mSettingsValues.mDurationOfFadeoutLanguageOnSpacebar);
+ break;
+ case MSG_DISMISS_LANGUAGE_ON_SPACEBAR:
+ if (inputView != null) {
+ inputView.setSpacebarTextFadeFactor(
+ mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar,
+ (LatinKeyboard)msg.obj);
+ }
+ break;
+ }
}
- public WordAlternatives(CharSequence chosenWord) {
- mChosenWord = chosenWord;
+ public void postUpdateSuggestions() {
+ removeMessages(MSG_UPDATE_SUGGESTIONS);
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS),
+ mSettingsValues.mDelayUpdateSuggestions);
}
- @Override
- public int hashCode() {
- return mChosenWord.hashCode();
+ public void cancelUpdateSuggestions() {
+ removeMessages(MSG_UPDATE_SUGGESTIONS);
}
- public abstract CharSequence getOriginalWord();
+ public boolean hasPendingUpdateSuggestions() {
+ return hasMessages(MSG_UPDATE_SUGGESTIONS);
+ }
- public CharSequence getChosenWord() {
- return mChosenWord;
+ public void postUpdateOldSuggestions() {
+ removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS),
+ mSettingsValues.mDelayUpdateOldSuggestions);
}
- public abstract List<CharSequence> getAlternatives();
- }
+ public void cancelUpdateOldSuggestions() {
+ removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
+ }
- public class TypedWordAlternatives extends WordAlternatives {
- private WordComposer word;
+ public void postUpdateShiftKeyState() {
+ removeMessages(MSG_UPDATE_SHIFT_STATE);
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE),
+ mSettingsValues.mDelayUpdateShiftState);
+ }
- public TypedWordAlternatives() {
- // Nothing
+ public void cancelUpdateShiftState() {
+ removeMessages(MSG_UPDATE_SHIFT_STATE);
}
- public TypedWordAlternatives(CharSequence chosenWord, WordComposer wordComposer) {
- super(chosenWord);
- word = wordComposer;
+ public void postUpdateBigramPredictions() {
+ removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
+ sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS),
+ mSettingsValues.mDelayUpdateSuggestions);
}
- @Override
- public CharSequence getOriginalWord() {
- return word.getTypedWord();
+ public void cancelUpdateBigramPredictions() {
+ removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
}
- @Override
- public List<CharSequence> getAlternatives() {
- return getTypedSuggestions(word);
+ public void updateVoiceResults() {
+ sendMessage(obtainMessage(MSG_VOICE_RESULTS));
}
- }
- /* package */ Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_UPDATE_SUGGESTIONS:
- updateSuggestions();
- break;
- case MSG_UPDATE_OLD_SUGGESTIONS:
- setOldSuggestions();
- break;
- case MSG_START_TUTORIAL:
- if (mTutorial == null) {
- if (mKeyboardSwitcher.getInputView().isShown()) {
- mTutorial = new Tutorial(
- LatinIME.this, mKeyboardSwitcher.getInputView());
- mTutorial.start();
- } else {
- // Try again soon if the view is not yet showing
- sendMessageDelayed(obtainMessage(MSG_START_TUTORIAL), 100);
- }
- }
- break;
- case MSG_UPDATE_SHIFT_STATE:
- updateShiftKeyState(getCurrentInputEditorInfo());
- break;
- case MSG_VOICE_RESULTS:
- handleVoiceResults();
- break;
+ public void startDisplayLanguageOnSpacebar(boolean localeChanged) {
+ removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR);
+ removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR);
+ final LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
+ if (inputView != null) {
+ final LatinKeyboard keyboard = mKeyboardSwitcher.getLatinKeyboard();
+ // The language is always displayed when the delay is negative.
+ final boolean needsToDisplayLanguage = localeChanged
+ || mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar < 0;
+ // The language is never displayed when the delay is zero.
+ if (mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar != 0) {
+ inputView.setSpacebarTextFadeFactor(needsToDisplayLanguage ? 1.0f
+ : mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar, keyboard);
+ }
+ // The fadeout animation will start when the delay is positive.
+ if (localeChanged && mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar > 0) {
+ sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard),
+ mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar);
+ }
}
}
- };
+
+ public void startDoubleSpacesTimer() {
+ removeMessages(MSG_SPACE_TYPED);
+ sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED),
+ mSettingsValues.mDoubleSpacesTurnIntoPeriodTimeout);
+ }
+
+ public void cancelDoubleSpacesTimer() {
+ removeMessages(MSG_SPACE_TYPED);
+ }
+
+ public boolean isAcceptingDoubleSpaces() {
+ return hasMessages(MSG_SPACE_TYPED);
+ }
+ }
@Override
public void onCreate() {
- LatinImeLogger.init(this);
- KeyboardSwitcher.init(this);
- super.onCreate();
- //setStatusIcon(R.drawable.ime_qwerty);
- mResources = getResources();
- final Configuration conf = mResources.getConfiguration();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- mLanguageSwitcher = new LanguageSwitcher(this);
- mLanguageSwitcher.loadLocales(prefs);
+ mPrefs = prefs;
+ LatinImeLogger.init(this, prefs);
+ LanguageSwitcherProxy.init(this, prefs);
+ SubtypeSwitcher.init(this, prefs);
+ KeyboardSwitcher.init(this, prefs);
+ Recorrection.init(this, prefs);
+ AccessibilityUtils.init(this, prefs);
+
+ super.onCreate();
+
+ mImm = InputMethodManagerCompatWrapper.getInstance(this);
+ mInputMethodId = Utils.getInputMethodId(mImm, getPackageName());
+ mSubtypeSwitcher = SubtypeSwitcher.getInstance();
mKeyboardSwitcher = KeyboardSwitcher.getInstance();
- mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher);
- mSystemLocale = conf.locale.toString();
- mLanguageSwitcher.setSystemLocale(conf.locale);
- String inputLanguage = mLanguageSwitcher.getInputLanguage();
- if (inputLanguage == null) {
- inputLanguage = conf.locale.toString();
- }
- mReCorrectionEnabled = prefs.getBoolean(PREF_RECORRECTION_ENABLED,
- getResources().getBoolean(R.bool.default_recorrection_enabled));
+ mRecorrection = Recorrection.getInstance();
+ DEBUG = LatinImeLogger.sDBG;
- LatinIMEUtil.GCUtils.getInstance().reset();
+ loadSettings();
+
+ final Resources res = getResources();
+ mResources = res;
+
+ Utils.GCUtils.getInstance().reset();
boolean tryGC = true;
- for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
+ for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
try {
- initSuggest(inputLanguage);
+ initSuggest();
tryGC = false;
} catch (OutOfMemoryError e) {
- tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(inputLanguage, e);
+ tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
}
}
- mOrientation = conf.orientation;
- initSuggestPuncList();
+ mOrientation = res.getConfiguration().orientation;
- // register to receive ringer mode changes for silent mode
- IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION);
+ // Register to receive ringer mode change and network state change.
+ // Also receive installation and removal of a dictionary pack.
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(mReceiver, filter);
- if (VOICE_INSTALLED) {
- mVoiceInput = new VoiceInput(this, this);
- mHints = new Hints(this, new Hints.Display() {
- public void showHint(int viewResource) {
- LayoutInflater inflater = (LayoutInflater) getSystemService(
- Context.LAYOUT_INFLATER_SERVICE);
- View view = inflater.inflate(viewResource, null);
- setCandidatesView(view);
- setCandidatesViewShown(true);
- mIsShowingHint = true;
- }
- });
- }
- prefs.registerOnSharedPreferenceChangeListener(this);
- }
+ mVoiceProxy = VoiceProxy.init(this, prefs, mHandler);
- /**
- * Loads a dictionary or multiple separated dictionary
- * @return returns array of dictionary resource ids
- */
- /* package */ static int[] getDictionary(Resources res) {
- String packageName = LatinIME.class.getPackage().getName();
- XmlResourceParser xrp = res.getXml(R.xml.dictionary);
- ArrayList<Integer> dictionaries = new ArrayList<Integer>();
-
- try {
- int current = xrp.getEventType();
- while (current != XmlResourceParser.END_DOCUMENT) {
- if (current == XmlResourceParser.START_TAG) {
- String tag = xrp.getName();
- if (tag != null) {
- if (tag.equals("part")) {
- String dictFileName = xrp.getAttributeValue(null, "name");
- dictionaries.add(res.getIdentifier(dictFileName, "raw", packageName));
- }
- }
- }
- xrp.next();
- current = xrp.getEventType();
- }
- } catch (XmlPullParserException e) {
- Log.e(TAG, "Dictionary XML parsing failure");
- } catch (IOException e) {
- Log.e(TAG, "Dictionary XML IOException");
- }
+ final IntentFilter packageFilter = new IntentFilter();
+ packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ packageFilter.addDataScheme(SCHEME_PACKAGE);
+ registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
- int count = dictionaries.size();
- int[] dict = new int[count];
- for (int i = 0; i < count; i++) {
- dict[i] = dictionaries.get(i);
- }
+ final IntentFilter newDictFilter = new IntentFilter();
+ newDictFilter.addAction(
+ DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION);
+ registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
+ }
- return dict;
+ // Has to be package-visible for unit tests
+ /* package */ void loadSettings() {
+ if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ if (null == mSubtypeSwitcher) mSubtypeSwitcher = SubtypeSwitcher.getInstance();
+ mSettingsValues = new Settings.Values(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr());
+ resetContactsDictionary();
}
- private void initSuggest(String locale) {
- mInputLocale = locale;
+ private void initSuggest() {
+ final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
+ final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr);
- Resources orig = getResources();
- Configuration conf = orig.getConfiguration();
- Locale saveLocale = conf.locale;
- conf.locale = new Locale(locale);
- orig.updateConfiguration(conf, orig.getDisplayMetrics());
+ final Resources res = mResources;
+ final Locale savedLocale = Utils.setSystemLocale(res, keyboardLocale);
if (mSuggest != null) {
mSuggest.close();
}
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
- mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true);
- int[] dictionaries = getDictionary(orig);
- mSuggest = new Suggest(this, dictionaries);
- updateAutoTextEnabled(saveLocale);
- if (mUserDictionary != null) mUserDictionary.close();
- mUserDictionary = new UserDictionary(this, mInputLocale);
- if (mContactsDictionary == null) {
- mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
- }
- if (mAutoDictionary != null) {
- mAutoDictionary.close();
+ int mainDicResId = Utils.getMainDictionaryResourceId(res);
+ mSuggest = new Suggest(this, mainDicResId, keyboardLocale);
+ if (mSettingsValues.mAutoCorrectEnabled) {
+ mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
}
- mAutoDictionary = new AutoDictionary(this, this, mInputLocale, Suggest.DIC_AUTO);
- if (mUserBigramDictionary != null) {
- mUserBigramDictionary.close();
- }
- mUserBigramDictionary = new UserBigramDictionary(this, this, mInputLocale,
- Suggest.DIC_USER);
- mSuggest.setUserBigramDictionary(mUserBigramDictionary);
+ updateAutoTextEnabled();
+
+ mUserDictionary = new UserDictionary(this, localeStr);
mSuggest.setUserDictionary(mUserDictionary);
- mSuggest.setContactsDictionary(mContactsDictionary);
+
+ resetContactsDictionary();
+
+ mAutoDictionary = new AutoDictionary(this, this, localeStr, Suggest.DIC_AUTO);
mSuggest.setAutoDictionary(mAutoDictionary);
+
+ mUserBigramDictionary = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER);
+ mSuggest.setUserBigramDictionary(mUserBigramDictionary);
+
updateCorrectionMode();
- mWordSeparators = mResources.getString(R.string.word_separators);
- mSentenceSeparators = mResources.getString(R.string.sentence_separators);
- conf.locale = saveLocale;
- orig.updateConfiguration(conf, orig.getDisplayMetrics());
+ Utils.setSystemLocale(res, savedLocale);
+ }
+
+ private void resetContactsDictionary() {
+ if (null == mSuggest) return;
+ ContactsDictionary contactsDictionary = mSettingsValues.mUseContactsDict
+ ? new ContactsDictionary(this, Suggest.DIC_CONTACTS) : null;
+ mSuggest.setContactsDictionary(contactsDictionary);
+ }
+
+ /* package private */ void resetSuggestMainDict() {
+ final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
+ final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr);
+ int mainDicResId = Utils.getMainDictionaryResourceId(mResources);
+ mSuggest.resetMainDict(this, mainDicResId, keyboardLocale);
}
@Override
public void onDestroy() {
- if (mUserDictionary != null) {
- mUserDictionary.close();
- }
- if (mContactsDictionary != null) {
- mContactsDictionary.close();
+ if (mSuggest != null) {
+ mSuggest.close();
+ mSuggest = null;
}
unregisterReceiver(mReceiver);
- if (VOICE_INSTALLED && mVoiceInput != null) {
- mVoiceInput.destroy();
- }
+ unregisterReceiver(mDictionaryPackInstallReceiver);
+ mVoiceProxy.destroy();
LatinImeLogger.commit();
LatinImeLogger.onDestroy();
super.onDestroy();
@@ -495,236 +474,192 @@ public class LatinIME extends InputMethodService
@Override
public void onConfigurationChanged(Configuration conf) {
- // If the system locale changes and is different from the saved
- // locale (mSystemLocale), then reload the input locale list from the
- // latin ime settings (shared prefs) and reset the input locale
- // to the first one.
- final String systemLocale = conf.locale.toString();
- if (!TextUtils.equals(systemLocale, mSystemLocale)) {
- mSystemLocale = systemLocale;
- if (mLanguageSwitcher != null) {
- mLanguageSwitcher.loadLocales(
- PreferenceManager.getDefaultSharedPreferences(this));
- mLanguageSwitcher.setSystemLocale(conf.locale);
- toggleLanguage(true, true);
- } else {
- reloadKeyboards();
- }
- }
+ mSubtypeSwitcher.onConfigurationChanged(conf);
// If orientation changed while predicting, commit the change
if (conf.orientation != mOrientation) {
InputConnection ic = getCurrentInputConnection();
commitTyped(ic);
if (ic != null) ic.finishComposingText(); // For voice input
mOrientation = conf.orientation;
- reloadKeyboards();
+ if (isShowingOptionDialog())
+ mOptionsDialog.dismiss();
}
+
mConfigurationChanging = true;
super.onConfigurationChanged(conf);
- if (mRecognizing) {
- switchToRecognitionStatusView();
- }
+ mVoiceProxy.onConfigurationChanged(conf);
mConfigurationChanging = false;
+
+ // This will work only when the subtype is not supported.
+ LanguageSwitcherProxy.onConfigurationChanged(conf);
}
@Override
public View onCreateInputView() {
- mKeyboardSwitcher.recreateInputView();
- mKeyboardSwitcher.makeKeyboards(true);
- mKeyboardSwitcher.setKeyboardMode(
- KeyboardSwitcher.MODE_TEXT, 0,
- shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo()));
- return mKeyboardSwitcher.getInputView();
+ return mKeyboardSwitcher.onCreateInputView();
}
@Override
- public View onCreateCandidatesView() {
- mKeyboardSwitcher.makeKeyboards(true);
- mCandidateViewContainer = (LinearLayout) getLayoutInflater().inflate(
- R.layout.candidates, null);
- mCandidateView = (CandidateView) mCandidateViewContainer.findViewById(R.id.candidates);
- mCandidateView.setService(this);
- setCandidatesViewShown(true);
- return mCandidateViewContainer;
+ public void setInputView(View view) {
+ super.setInputView(view);
+ mCandidateViewContainer = view.findViewById(R.id.candidates_container);
+ mCandidateView = (CandidateView) view.findViewById(R.id.candidates);
+ mCandidateView.setListener(this, view);
+ mCandidateStripHeight = (int)mResources.getDimension(R.dimen.candidate_strip_height);
+ }
+
+ @Override
+ public void setCandidatesView(View view) {
+ // To ensure that CandidatesView will never be set.
+ return;
}
@Override
public void onStartInputView(EditorInfo attribute, boolean restarting) {
- LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
+ final KeyboardSwitcher switcher = mKeyboardSwitcher;
+ LatinKeyboardView inputView = switcher.getKeyboardView();
+
+ if (DEBUG) {
+ Log.d(TAG, "onStartInputView: attribute:" + ((attribute == null) ? "none"
+ : String.format("inputType=0x%08x imeOptions=0x%08x",
+ attribute.inputType, attribute.imeOptions)));
+ }
// In landscape mode, this method gets called without the input view being created.
if (inputView == null) {
return;
}
- if (mRefreshKeyboardRequired) {
- mRefreshKeyboardRequired = false;
- toggleLanguage(true, true);
- }
-
- mKeyboardSwitcher.makeKeyboards(false);
-
- TextEntryState.newSession(this);
+ mSubtypeSwitcher.updateParametersOnStartInputView();
- // Most such things we decide below in the switch statement, but we need to know
- // now whether this is a password text field, because we need to know now (before
- // the switch statement) whether we want to enable the voice button.
- mPasswordText = false;
- int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION;
- if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD ||
- variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) {
- mPasswordText = true;
- }
+ TextEntryState.reset();
- mEnableVoiceButton = shouldShowVoiceButton(makeFieldContext(), attribute);
- final boolean enableVoiceButton = mEnableVoiceButton && mEnableVoice;
+ // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to
+ // know now whether this is a password text field, because we need to know now whether we
+ // want to enable the voice button.
+ final VoiceProxy voiceIme = mVoiceProxy;
+ voiceIme.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(attribute.inputType)
+ || InputTypeCompatUtils.isVisiblePasswordInputType(attribute.inputType));
- mAfterVoiceInput = false;
- mImmediatelyAfterVoiceInput = false;
- mShowingVoiceSuggestions = false;
- mVoiceInputHighlighted = false;
- mInputTypeNoAutoCorrect = false;
- mPredictionOn = false;
- mCompletionOn = false;
- mCompletions = null;
- mCapsLock = false;
- mEnteredText = null;
-
- switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) {
- case EditorInfo.TYPE_CLASS_NUMBER:
- case EditorInfo.TYPE_CLASS_DATETIME:
- // fall through
- // NOTE: For now, we use the phone keyboard for NUMBER and DATETIME until we get
- // a dedicated number entry keypad.
- // TODO: Use a dedicated number entry keypad here when we get one.
- case EditorInfo.TYPE_CLASS_PHONE:
- mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE,
- attribute.imeOptions, enableVoiceButton);
- break;
- case EditorInfo.TYPE_CLASS_TEXT:
- mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT,
- attribute.imeOptions, enableVoiceButton);
- //startPrediction();
- mPredictionOn = true;
- // Make sure that passwords are not displayed in candidate view
- if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD ||
- variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ) {
- mPredictionOn = false;
- }
- if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
- || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) {
- mAutoSpace = false;
- } else {
- mAutoSpace = true;
- }
- if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) {
- mPredictionOn = false;
- mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL,
- attribute.imeOptions, enableVoiceButton);
- } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) {
- mPredictionOn = false;
- mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL,
- attribute.imeOptions, enableVoiceButton);
- } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
- mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM,
- attribute.imeOptions, enableVoiceButton);
- } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) {
- mPredictionOn = false;
- } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) {
- mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_WEB,
- attribute.imeOptions, enableVoiceButton);
- // If it's a browser edit field and auto correct is not ON explicitly, then
- // disable auto correction, but keep suggestions on.
- if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) {
- mInputTypeNoAutoCorrect = true;
- }
- }
+ initializeInputAttributes(attribute);
- // If NO_SUGGESTIONS is set, don't do prediction.
- if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
- mPredictionOn = false;
- mInputTypeNoAutoCorrect = true;
- }
- // If it's not multiline and the autoCorrect flag is not set, then don't correct
- if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 &&
- (attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
- mInputTypeNoAutoCorrect = true;
- }
- if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
- mPredictionOn = false;
- mCompletionOn = isFullscreenMode();
- }
- break;
- default:
- mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT,
- attribute.imeOptions, enableVoiceButton);
- }
inputView.closing();
+ mEnteredText = null;
mComposing.setLength(0);
- mPredicting = false;
+ mHasUncommittedTypedChars = false;
mDeleteCount = 0;
- mJustAddedAutoSpace = false;
+ mJustAddedMagicSpace = false;
+ mJustReplacedDoubleSpace = false;
+
loadSettings();
- updateShiftKeyState(attribute);
+ updateCorrectionMode();
+ updateAutoTextEnabled();
+ updateSuggestionVisibility(mPrefs, mResources);
- setCandidatesViewShownInternal(isCandidateStripVisible() || mCompletionOn,
- false /* needsInputViewShown */ );
- updateSuggestions();
+ if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
+ mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+ }
+ mVoiceProxy.loadSettings(attribute, mPrefs);
+ // This will work only when the subtype is not supported.
+ LanguageSwitcherProxy.loadSettings();
- // If the dictionary is not big enough, don't auto correct
- mHasDictionary = mSuggest.hasMainDictionary();
+ if (mSubtypeSwitcher.isKeyboardMode()) {
+ switcher.loadKeyboard(attribute,
+ mSubtypeSwitcher.isShortcutImeEnabled() && voiceIme.isVoiceButtonEnabled(),
+ voiceIme.isVoiceButtonOnPrimary());
+ switcher.updateShiftState();
+ }
+
+ setSuggestionStripShownInternal(isCandidateStripVisible(), /* needsInputViewShown */ false);
+ // Delay updating suggestions because keyboard input view may not be shown at this point.
+ mHandler.postUpdateSuggestions();
updateCorrectionMode();
- inputView.setPreviewEnabled(mPopupOn);
+ inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
+ mSettingsValues.mKeyPreviewPopupDismissDelay);
inputView.setProximityCorrectionEnabled(true);
- mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || mShowSuggestions);
// If we just entered a text field, maybe it has some old text that requires correction
- checkReCorrectionOnStart();
- checkTutorial(attribute.privateImeOptions);
+ mRecorrection.checkRecorrectionOnStart();
+ inputView.setForeground(true);
+
+ voiceIme.onStartInputView(inputView.getWindowToken());
+
if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
}
- private void checkReCorrectionOnStart() {
- if (mReCorrectionEnabled && isPredictionOn()) {
- // 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/Stop because onUpdateSelection() has
- // not been called yet.
- InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- ExtractedTextRequest etr = new ExtractedTextRequest();
- etr.token = 0; // anything is fine here
- ExtractedText et = ic.getExtractedText(etr, 0);
- if (et == null) return;
-
- mLastSelectionStart = et.startOffset + et.selectionStart;
- mLastSelectionEnd = et.startOffset + et.selectionEnd;
-
- // Then look for possible corrections in a delayed fashion
- if (!TextUtils.isEmpty(et.text) && isCursorTouchingWord()) {
- postUpdateOldSuggestions();
+ private void initializeInputAttributes(EditorInfo attribute) {
+ if (attribute == null)
+ return;
+ final int inputType = attribute.inputType;
+ final int variation = inputType & InputType.TYPE_MASK_VARIATION;
+ mShouldInsertMagicSpace = false;
+ mInputTypeNoAutoCorrect = false;
+ mIsSettingsSuggestionStripOn = false;
+ mApplicationSpecifiedCompletionOn = false;
+ mApplicationSpecifiedCompletions = null;
+
+ if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
+ mIsSettingsSuggestionStripOn = true;
+ // Make sure that passwords are not displayed in candidate view
+ if (InputTypeCompatUtils.isPasswordInputType(inputType)
+ || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)) {
+ mIsSettingsSuggestionStripOn = false;
+ }
+ if (InputTypeCompatUtils.isEmailVariation(variation)
+ || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) {
+ mShouldInsertMagicSpace = false;
+ } else {
+ mShouldInsertMagicSpace = true;
+ }
+ if (InputTypeCompatUtils.isEmailVariation(variation)) {
+ mIsSettingsSuggestionStripOn = false;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
+ mIsSettingsSuggestionStripOn = false;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+ mIsSettingsSuggestionStripOn = false;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) {
+ // If it's a browser edit field and auto correct is not ON explicitly, then
+ // disable auto correction, but keep suggestions on.
+ if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) {
+ mInputTypeNoAutoCorrect = true;
+ }
+ }
+
+ // If NO_SUGGESTIONS is set, don't do prediction.
+ if ((inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
+ mIsSettingsSuggestionStripOn = false;
+ mInputTypeNoAutoCorrect = true;
+ }
+ // If it's not multiline and the autoCorrect flag is not set, then don't correct
+ if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0
+ && (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
+ mInputTypeNoAutoCorrect = true;
+ }
+ if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
+ mIsSettingsSuggestionStripOn = false;
+ mApplicationSpecifiedCompletionOn = isFullscreenMode();
}
}
}
@Override
+ public void onWindowHidden() {
+ super.onWindowHidden();
+ KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
+ if (inputView != null) inputView.closing();
+ }
+
+ @Override
public void onFinishInput() {
super.onFinishInput();
LatinImeLogger.commit();
- onAutoCompletionStateChanged(false);
+ mKeyboardSwitcher.onAutoCorrectionStateChanged(false);
- if (VOICE_INSTALLED && !mConfigurationChanging) {
- if (mAfterVoiceInput) {
- mVoiceInput.flushAllTextModificationCounters();
- mVoiceInput.logInputEnded();
- }
- mVoiceInput.flushLogs();
- mVoiceInput.cancel();
- }
- if (mKeyboardSwitcher.getInputView() != null) {
- mKeyboardSwitcher.getInputView().closing();
- }
+ mVoiceProxy.flushVoiceInputLogs(mConfigurationChanging);
+
+ KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
+ if (inputView != null) inputView.closing();
if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites();
if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites();
}
@@ -732,21 +667,17 @@ public class LatinIME extends InputMethodService
@Override
public void onFinishInputView(boolean finishingInput) {
super.onFinishInputView(finishingInput);
- // Remove penging messages related to update suggestions
- mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS);
- mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
+ KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
+ if (inputView != null) inputView.setForeground(false);
+ // Remove pending messages related to update suggestions
+ mHandler.cancelUpdateSuggestions();
+ mHandler.cancelUpdateOldSuggestions();
}
@Override
public void onUpdateExtractedText(int token, ExtractedText text) {
super.onUpdateExtractedText(token, text);
- InputConnection ic = getCurrentInputConnection();
- if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) {
- if (mHints.showPunctuationHintIfNecessary(ic)) {
- mVoiceInput.logPunctuationHintDisplayed();
- }
- }
- mImmediatelyAfterVoiceInput = false;
+ mVoiceProxy.showPunctuationHintIfNecessary();
}
@Override
@@ -759,75 +690,70 @@ public class LatinIME extends InputMethodService
if (DEBUG) {
Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
+ ", ose=" + oldSelEnd
+ + ", lss=" + mLastSelectionStart
+ + ", lse=" + mLastSelectionEnd
+ ", nss=" + newSelStart
+ ", nse=" + newSelEnd
+ ", cs=" + candidatesStart
+ ", ce=" + candidatesEnd);
}
- if (mAfterVoiceInput) {
- mVoiceInput.setCursorPos(newSelEnd);
- mVoiceInput.setSelectionSpan(newSelEnd - newSelStart);
- }
+ mVoiceProxy.setCursorAndSelection(newSelEnd, newSelStart);
// If the current selection in the text view changes, we should
// clear whatever candidate text we have.
- if ((((mComposing.length() > 0 && mPredicting) || mVoiceInputHighlighted)
- && (newSelStart != candidatesEnd
- || newSelEnd != candidatesEnd)
- && mLastSelectionStart != newSelStart)) {
+ final boolean selectionChanged = (newSelStart != candidatesEnd
+ || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart;
+ final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1;
+ if (((mComposing.length() > 0 && mHasUncommittedTypedChars)
+ || mVoiceProxy.isVoiceInputHighlighted())
+ && (selectionChanged || candidatesCleared)) {
+ if (candidatesCleared) {
+ // If the composing span has been cleared, save the typed word in the history for
+ // recorrection before we reset the candidate strip. Then, we'll be able to show
+ // suggestions for recorrection right away.
+ mRecorrection.saveRecorrectionSuggestion(mWord, mComposing);
+ }
mComposing.setLength(0);
- mPredicting = false;
- postUpdateSuggestions();
+ mHasUncommittedTypedChars = false;
+ if (isCursorTouchingWord()) {
+ mHandler.cancelUpdateBigramPredictions();
+ mHandler.postUpdateSuggestions();
+ } else {
+ setPunctuationSuggestions();
+ }
TextEntryState.reset();
InputConnection ic = getCurrentInputConnection();
if (ic != null) {
ic.finishComposingText();
}
- mVoiceInputHighlighted = false;
- } else if (!mPredicting && !mJustAccepted) {
- switch (TextEntryState.getState()) {
- case ACCEPTED_DEFAULT:
+ mVoiceProxy.setVoiceInputHighlighted(false);
+ } else if (!mHasUncommittedTypedChars && !mExpectingUpdateSelection) {
+ if (TextEntryState.isAcceptedDefault() || TextEntryState.isSpaceAfterPicked()) {
+ if (TextEntryState.isAcceptedDefault())
TextEntryState.reset();
- // fall through
- case SPACE_AFTER_PICKED:
- mJustAddedAutoSpace = false; // The user moved the cursor.
- break;
}
}
- mJustAccepted = false;
- postUpdateShiftKeyState();
+ if (!mExpectingUpdateSelection) {
+ mJustAddedMagicSpace = false; // The user moved the cursor.
+ mJustReplacedDoubleSpace = false;
+ }
+ mExpectingUpdateSelection = false;
+ mHandler.postUpdateShiftKeyState();
// Make a note of the cursor position
mLastSelectionStart = newSelStart;
mLastSelectionEnd = newSelEnd;
- if (mReCorrectionEnabled) {
- // Don't look for corrections if the keyboard is not visible
- if (mKeyboardSwitcher != null && mKeyboardSwitcher.getInputView() != null
- && mKeyboardSwitcher.getInputView().isShown()) {
- // Check if we should go in or out of correction mode.
- if (isPredictionOn()
- && mJustRevertedSeparator == null
- && (candidatesStart == candidatesEnd || newSelStart != oldSelStart
- || TextEntryState.isCorrecting())
- && (newSelStart < newSelEnd - 1 || (!mPredicting))
- && !mVoiceInputHighlighted) {
- if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) {
- postUpdateOldSuggestions();
- } else {
- abortCorrection(false);
- // Show the punctuation suggestions list if the current one is not
- // and if not showing "Touch again to save".
- if (mCandidateView != null
- && !mSuggestPuncList.equals(mCandidateView.getSuggestions())
- && !mCandidateView.isShowingAddToDictionaryHint()) {
- setNextSuggestions();
- }
- }
- }
- }
- }
+ mRecorrection.updateRecorrectionSelection(mKeyboardSwitcher,
+ mCandidateView, candidatesStart, candidatesEnd, newSelStart,
+ newSelEnd, oldSelStart, mLastSelectionStart,
+ mLastSelectionEnd, mHasUncommittedTypedChars);
+ }
+
+ public void setLastSelection(int start, int end) {
+ mLastSelectionStart = start;
+ mLastSelectionEnd = end;
}
/**
@@ -840,7 +766,7 @@ public class LatinIME extends InputMethodService
*/
@Override
public void onExtractedTextClicked() {
- if (mReCorrectionEnabled && isPredictionOn()) return;
+ if (mRecorrection.isRecorrectionEnabled() && isSuggestionsRequested()) return;
super.onExtractedTextClicked();
}
@@ -856,7 +782,7 @@ public class LatinIME extends InputMethodService
*/
@Override
public void onExtractedCursorMovement(int dx, int dy) {
- if (mReCorrectionEnabled && isPredictionOn()) return;
+ if (mRecorrection.isRecorrectionEnabled() && isSuggestionsRequested()) return;
super.onExtractedCursorMovement(dx, dy);
}
@@ -864,84 +790,103 @@ public class LatinIME extends InputMethodService
@Override
public void hideWindow() {
LatinImeLogger.commit();
- onAutoCompletionStateChanged(false);
+ mKeyboardSwitcher.onAutoCorrectionStateChanged(false);
if (TRACE) Debug.stopMethodTracing();
if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
mOptionsDialog.dismiss();
mOptionsDialog = null;
}
- if (!mConfigurationChanging) {
- if (mAfterVoiceInput) mVoiceInput.logInputEnded();
- if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
- mVoiceInput.logKeyboardWarningDialogDismissed();
- mVoiceWarningDialog.dismiss();
- mVoiceWarningDialog = null;
- }
- if (VOICE_INSTALLED & mRecognizing) {
- mVoiceInput.cancel();
- }
- }
- mWordToSuggestions.clear();
- mWordHistory.clear();
+ mVoiceProxy.hideVoiceWindow(mConfigurationChanging);
+ mRecorrection.clearWordsInHistory();
super.hideWindow();
- TextEntryState.endSession();
}
@Override
- public void onDisplayCompletions(CompletionInfo[] completions) {
+ public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
if (DEBUG) {
- Log.i("foo", "Received completions:");
- for (int i=0; i<(completions != null ? completions.length : 0); i++) {
- Log.i("foo", " #" + i + ": " + completions[i]);
+ Log.i(TAG, "Received completions:");
+ if (applicationSpecifiedCompletions != null) {
+ for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
+ Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]);
+ }
}
}
- if (mCompletionOn) {
- mCompletions = completions;
- if (completions == null) {
+ if (mApplicationSpecifiedCompletionOn) {
+ mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
+ if (applicationSpecifiedCompletions == null) {
clearSuggestions();
return;
}
- List<CharSequence> stringList = new ArrayList<CharSequence>();
- for (int i=0; i<(completions != null ? completions.length : 0); i++) {
- CompletionInfo ci = completions[i];
- if (ci != null) stringList.add(ci.getText());
- }
+ SuggestedWords.Builder builder = new SuggestedWords.Builder()
+ .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions)
+ .setTypedWordValid(true)
+ .setHasMinimalSuggestion(true);
// When in fullscreen mode, show completions generated by the application
- setSuggestions(stringList, true, true, true);
+ setSuggestions(builder.build());
mBestWord = null;
- setCandidatesViewShown(true);
+ setSuggestionStripShown(true);
}
}
- private void setCandidatesViewShownInternal(boolean shown, boolean needsInputViewShown) {
- // TODO: Remove this if we support candidates with hard keyboard
+ private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
+ // TODO: Modify this if we support candidates with hard keyboard
if (onEvaluateInputViewShown()) {
- super.setCandidatesViewShown(shown && mKeyboardSwitcher.getInputView() != null
- && (needsInputViewShown ? mKeyboardSwitcher.getInputView().isShown() : true));
+ final boolean shouldShowCandidates = shown
+ && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true);
+ if (isExtractViewShown()) {
+ // No need to have extra space to show the key preview.
+ mCandidateViewContainer.setMinimumHeight(0);
+ mCandidateViewContainer.setVisibility(
+ shouldShowCandidates ? View.VISIBLE : View.GONE);
+ } else {
+ // We must control the visibility of the suggestion strip in order to avoid clipped
+ // key previews, even when we don't show the suggestion strip.
+ mCandidateViewContainer.setVisibility(
+ shouldShowCandidates ? View.VISIBLE : View.INVISIBLE);
+ }
}
}
- @Override
- public void setCandidatesViewShown(boolean shown) {
- setCandidatesViewShownInternal(shown, true /* needsInputViewShown */ );
+ private void setSuggestionStripShown(boolean shown) {
+ setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
}
@Override
public void onComputeInsets(InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
- if (!isFullscreenMode()) {
- outInsets.contentTopInsets = outInsets.visibleTopInsets;
+ final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
+ if (inputView == null || mCandidateViewContainer == null)
+ return;
+ final int containerHeight = mCandidateViewContainer.getHeight();
+ int touchY = containerHeight;
+ // Need to set touchable region only if input view is being shown
+ if (mKeyboardSwitcher.isInputViewShown()) {
+ if (mCandidateViewContainer.getVisibility() == View.VISIBLE) {
+ touchY -= mCandidateStripHeight;
+ }
+ final int touchWidth = inputView.getWidth();
+ final int touchHeight = inputView.getHeight() + containerHeight
+ // Extend touchable region below the keyboard.
+ + EXTENDED_TOUCHABLE_REGION_HEIGHT;
+ if (DEBUG) {
+ Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth
+ + " height=" + touchHeight);
+ }
+ setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight);
}
+ outInsets.contentTopInsets = touchY;
+ outInsets.visibleTopInsets = touchY;
}
@Override
public boolean onEvaluateFullscreenMode() {
- DisplayMetrics dm = getResources().getDisplayMetrics();
+ final Resources res = mResources;
+ DisplayMetrics dm = res.getDisplayMetrics();
float displayHeight = dm.heightPixels;
// If the display is more than X inches high, don't go to fullscreen mode
- float dimen = getResources().getDimension(R.dimen.max_height_for_fullscreen);
+ float dimen = res.getDimension(R.dimen.max_height_for_fullscreen);
if (displayHeight > dimen) {
return false;
} else {
@@ -952,25 +897,13 @@ public class LatinIME extends InputMethodService
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) {
- if (mKeyboardSwitcher.getInputView().handleBack()) {
- return true;
- } else if (mTutorial != null) {
- mTutorial.close();
- mTutorial = null;
- }
- }
- break;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_LEFT:
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- // If tutorial is visible, don't allow dpad to work
- if (mTutorial != null) {
+ case KeyEvent.KEYCODE_BACK:
+ if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getKeyboardView() != null) {
+ if (mKeyboardSwitcher.getKeyboardView().handleBack()) {
return true;
}
- break;
+ }
+ break;
}
return super.onKeyDown(keyCode, event);
}
@@ -978,138 +911,86 @@ public class LatinIME extends InputMethodService
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_LEFT:
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- // If tutorial is visible, don't allow dpad to work
- if (mTutorial != null) {
- return true;
- }
- LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
- // Enable shift key and DPAD to do selections
- if (inputView != null && inputView.isShown()
- && inputView.isShifted()) {
- event = new KeyEvent(event.getDownTime(), event.getEventTime(),
- event.getAction(), event.getKeyCode(), event.getRepeatCount(),
- event.getDeviceId(), event.getScanCode(),
- KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
- InputConnection ic = getCurrentInputConnection();
- if (ic != null) ic.sendKeyEvent(event);
- return true;
- }
- break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ // Enable shift key and DPAD to do selections
+ if (mKeyboardSwitcher.isInputViewShown()
+ && mKeyboardSwitcher.isShiftedOrShiftLocked()) {
+ KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
+ event.getAction(), event.getKeyCode(), event.getRepeatCount(),
+ event.getDeviceId(), event.getScanCode(),
+ KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null)
+ ic.sendKeyEvent(newEvent);
+ return true;
+ }
+ break;
}
return super.onKeyUp(keyCode, event);
}
- private void revertVoiceInput() {
- InputConnection ic = getCurrentInputConnection();
- if (ic != null) ic.commitText("", 1);
- updateSuggestions();
- mVoiceInputHighlighted = false;
- }
-
- private void commitVoiceInput() {
- InputConnection ic = getCurrentInputConnection();
- if (ic != null) ic.finishComposingText();
- updateSuggestions();
- mVoiceInputHighlighted = false;
- }
-
- private void reloadKeyboards() {
- mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher);
- if (mKeyboardSwitcher.getInputView() != null
- && mKeyboardSwitcher.getKeyboardMode() != KeyboardSwitcher.MODE_NONE) {
- mKeyboardSwitcher.setVoiceMode(mEnableVoice && mEnableVoiceButton, mVoiceOnPrimary);
- }
- mKeyboardSwitcher.makeKeyboards(true);
- }
-
- private void commitTyped(InputConnection inputConnection) {
- if (mPredicting) {
- mPredicting = false;
+ public void commitTyped(InputConnection inputConnection) {
+ if (mHasUncommittedTypedChars) {
+ mHasUncommittedTypedChars = false;
if (mComposing.length() > 0) {
if (inputConnection != null) {
inputConnection.commitText(mComposing, 1);
}
mCommittedLength = mComposing.length();
TextEntryState.acceptedTyped(mComposing);
- addToDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED);
+ addToAutoAndUserBigramDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED);
}
updateSuggestions();
}
}
- private void postUpdateShiftKeyState() {
- mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE);
- // TODO: Should remove this 300ms delay?
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SHIFT_STATE), 300);
- }
-
- public void updateShiftKeyState(EditorInfo attr) {
+ public boolean getCurrentAutoCapsState() {
InputConnection ic = getCurrentInputConnection();
- if (ic != null && attr != null && mKeyboardSwitcher.isAlphabetMode()) {
- mKeyboardSwitcher.setShifted(mShiftKeyState.isMomentary() || mCapsLock
- || getCursorCapsMode(ic, attr) != 0);
- }
- }
-
- private int getCursorCapsMode(InputConnection ic, EditorInfo attr) {
- int caps = 0;
EditorInfo ei = getCurrentInputEditorInfo();
- if (mAutoCap && ei != null && ei.inputType != EditorInfo.TYPE_NULL) {
- caps = ic.getCursorCapsMode(attr.inputType);
+ if (mSettingsValues.mAutoCap && ic != null && ei != null
+ && ei.inputType != InputType.TYPE_NULL) {
+ return ic.getCursorCapsMode(ei.inputType) != 0;
}
- return caps;
+ return false;
}
- private void swapPunctuationAndSpace() {
+ private void swapSwapperAndSpace() {
final InputConnection ic = getCurrentInputConnection();
if (ic == null) return;
CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
+ // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
if (lastTwo != null && lastTwo.length() == 2
- && lastTwo.charAt(0) == KEYCODE_SPACE && isSentenceSeparator(lastTwo.charAt(1))) {
+ && lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
ic.beginBatchEdit();
ic.deleteSurroundingText(2, 0);
ic.commitText(lastTwo.charAt(1) + " ", 1);
ic.endBatchEdit();
- updateShiftKeyState(getCurrentInputEditorInfo());
- mJustAddedAutoSpace = true;
+ mKeyboardSwitcher.updateShiftState();
}
}
- private void reswapPeriodAndSpace() {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
- if (lastThree != null && lastThree.length() == 3
- && lastThree.charAt(0) == KEYCODE_PERIOD
- && lastThree.charAt(1) == KEYCODE_SPACE
- && lastThree.charAt(2) == KEYCODE_PERIOD) {
- ic.beginBatchEdit();
- ic.deleteSurroundingText(3, 0);
- ic.commitText(" ..", 1);
- ic.endBatchEdit();
- updateShiftKeyState(getCurrentInputEditorInfo());
- }
- }
-
- private void doubleSpace() {
- //if (!mAutoPunctuate) return;
+ private void maybeDoubleSpace() {
if (mCorrectionMode == Suggest.CORRECTION_NONE) return;
final InputConnection ic = getCurrentInputConnection();
if (ic == null) return;
CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
if (lastThree != null && lastThree.length() == 3
&& Character.isLetterOrDigit(lastThree.charAt(0))
- && lastThree.charAt(1) == KEYCODE_SPACE && lastThree.charAt(2) == KEYCODE_SPACE) {
+ && lastThree.charAt(1) == Keyboard.CODE_SPACE
+ && lastThree.charAt(2) == Keyboard.CODE_SPACE
+ && mHandler.isAcceptingDoubleSpaces()) {
+ mHandler.cancelDoubleSpacesTimer();
ic.beginBatchEdit();
ic.deleteSurroundingText(2, 0);
ic.commitText(". ", 1);
ic.endBatchEdit();
- updateShiftKeyState(getCurrentInputEditorInfo());
- mJustAddedAutoSpace = true;
+ mKeyboardSwitcher.updateShiftState();
+ mJustReplacedDoubleSpace = true;
+ } else {
+ mHandler.startDoubleSpacesTimer();
}
}
@@ -1121,8 +1002,8 @@ public class LatinIME extends InputMethodService
// if there is one.
CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
if (lastOne != null && lastOne.length() == 1
- && lastOne.charAt(0) == KEYCODE_PERIOD
- && text.charAt(0) == KEYCODE_PERIOD) {
+ && lastOne.charAt(0) == Keyboard.CODE_PERIOD
+ && text.charAt(0) == Keyboard.CODE_PERIOD) {
ic.deleteSurroundingText(1, 0);
}
}
@@ -1133,16 +1014,17 @@ public class LatinIME extends InputMethodService
CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
if (lastOne != null && lastOne.length() == 1
- && lastOne.charAt(0) == KEYCODE_SPACE) {
+ && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
ic.deleteSurroundingText(1, 0);
}
}
+ @Override
public boolean addWordToDictionary(String word) {
mUserDictionary.addWord(word, 128);
// Suggestion strip should be updated after the operation of adding word to the
// user dictionary
- postUpdateSuggestions();
+ mHandler.postUpdateSuggestions();
return true;
}
@@ -1154,25 +1036,22 @@ public class LatinIME extends InputMethodService
}
}
- private void showInputMethodPicker() {
- ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
- .showInputMethodPicker();
- }
-
- private void onOptionKeyPressed() {
- if (!isShowingOptionDialog()) {
- if (LatinIMEUtil.hasMultipleEnabledIMEs(this)) {
- showOptionsMenu();
- } else {
- launchSettings();
- }
+ private void onSettingsKeyPressed() {
+ if (isShowingOptionDialog())
+ return;
+ if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
+ showSubtypeSelectorAndSettings();
+ } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) {
+ showOptionsMenu();
+ } else {
+ launchSettings();
}
}
- private void onOptionKeyLongPressed() {
+ private void onSettingsKeyLongPressed() {
if (!isShowingOptionDialog()) {
- if (LatinIMEUtil.hasMultipleEnabledIMEs(this)) {
- showInputMethodPicker();
+ if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) {
+ mImm.showInputMethodPicker();
} else {
launchSettings();
}
@@ -1183,150 +1062,155 @@ public class LatinIME extends InputMethodService
return mOptionsDialog != null && mOptionsDialog.isShowing();
}
- // Implementation of KeyboardViewListener
-
- public void onKey(int primaryCode, int[] keyCodes, int x, int y) {
+ // Implementation of {@link KeyboardActionListener}.
+ @Override
+ public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
long when = SystemClock.uptimeMillis();
- if (primaryCode != Keyboard.KEYCODE_DELETE ||
- when > mLastKeyTime + QUICK_PRESS) {
+ if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
mDeleteCount = 0;
}
mLastKeyTime = when;
- final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch();
+ KeyboardSwitcher switcher = mKeyboardSwitcher;
+ final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
+ final boolean lastStateOfJustReplacedDoubleSpace = mJustReplacedDoubleSpace;
+ mJustReplacedDoubleSpace = false;
switch (primaryCode) {
- case Keyboard.KEYCODE_DELETE:
- handleBackspace();
- mDeleteCount++;
- LatinImeLogger.logOnDelete();
- break;
- case Keyboard.KEYCODE_SHIFT:
- // Shift key is handled in onPress() when device has distinct multi-touch panel.
- if (!distinctMultiTouch)
- handleShift();
- break;
- case Keyboard.KEYCODE_MODE_CHANGE:
- // Symbol key is handled in onPress() when device has distinct multi-touch panel.
- if (!distinctMultiTouch)
- changeKeyboardMode();
- break;
- case Keyboard.KEYCODE_CANCEL:
- if (!isShowingOptionDialog()) {
- handleClose();
- }
- break;
- case LatinKeyboardView.KEYCODE_OPTIONS:
- onOptionKeyPressed();
- break;
- case LatinKeyboardView.KEYCODE_OPTIONS_LONGPRESS:
- onOptionKeyLongPressed();
- break;
- case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE:
- toggleLanguage(false, true);
- break;
- case LatinKeyboardView.KEYCODE_PREV_LANGUAGE:
- toggleLanguage(false, false);
- break;
- case LatinKeyboardView.KEYCODE_VOICE:
- if (VOICE_INSTALLED) {
- startListening(false /* was a button press, was not a swipe */);
- }
- break;
- case 9 /*Tab*/:
- sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
- break;
- default:
- if (primaryCode != KEYCODE_ENTER) {
- mJustAddedAutoSpace = false;
- }
- RingCharBuffer.getInstance().push((char)primaryCode, x, y);
- LatinImeLogger.logOnInputChar();
- if (isWordSeparator(primaryCode)) {
- handleSeparator(primaryCode);
- } else {
- handleCharacter(primaryCode, keyCodes);
- }
- // Cancel the just reverted state
- mJustRevertedSeparator = null;
+ case Keyboard.CODE_DELETE:
+ handleBackspace(lastStateOfJustReplacedDoubleSpace);
+ mDeleteCount++;
+ mExpectingUpdateSelection = true;
+ LatinImeLogger.logOnDelete();
+ break;
+ case Keyboard.CODE_SHIFT:
+ // Shift key is handled in onPress() when device has distinct multi-touch panel.
+ if (!distinctMultiTouch)
+ switcher.toggleShift();
+ break;
+ case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
+ // Symbol key is handled in onPress() when device has distinct multi-touch panel.
+ if (!distinctMultiTouch)
+ switcher.changeKeyboardMode();
+ break;
+ case Keyboard.CODE_CANCEL:
+ if (!isShowingOptionDialog()) {
+ handleClose();
+ }
+ break;
+ case Keyboard.CODE_SETTINGS:
+ onSettingsKeyPressed();
+ break;
+ case Keyboard.CODE_SETTINGS_LONGPRESS:
+ onSettingsKeyLongPressed();
+ break;
+ case LatinKeyboard.CODE_NEXT_LANGUAGE:
+ toggleLanguage(true);
+ break;
+ case LatinKeyboard.CODE_PREV_LANGUAGE:
+ toggleLanguage(false);
+ break;
+ case Keyboard.CODE_CAPSLOCK:
+ switcher.toggleCapsLock();
+ break;
+ case Keyboard.CODE_SHORTCUT:
+ mSubtypeSwitcher.switchToShortcutIME();
+ break;
+ case Keyboard.CODE_TAB:
+ handleTab();
+ // There are two cases for tab. Either we send a "next" event, that may change the
+ // focus but will never move the cursor. Or, we send a real tab keycode, which some
+ // applications may accept or ignore, and we don't know whether this will move the
+ // cursor or not. So actually, we don't really know.
+ // So to go with the safer option, we'd rather behave as if the user moved the
+ // cursor when they didn't than the opposite. We also expect that most applications
+ // will actually use tab only for focus movement.
+ // To sum it up: do not update mExpectingUpdateSelection here.
+ break;
+ default:
+ if (mSettingsValues.isWordSeparator(primaryCode)) {
+ handleSeparator(primaryCode, x, y);
+ } else {
+ handleCharacter(primaryCode, keyCodes, x, y);
+ }
+ mExpectingUpdateSelection = true;
+ break;
}
- mKeyboardSwitcher.onKey(primaryCode);
+ switcher.onKey(primaryCode);
// Reset after any single keystroke
mEnteredText = null;
}
- public void onText(CharSequence text) {
- if (VOICE_INSTALLED && mVoiceInputHighlighted) {
- commitVoiceInput();
- }
+ @Override
+ public void onTextInput(CharSequence text) {
+ mVoiceProxy.commitVoiceInput();
InputConnection ic = getCurrentInputConnection();
if (ic == null) return;
- abortCorrection(false);
+ mRecorrection.abortRecorrection(false);
ic.beginBatchEdit();
- if (mPredicting) {
- commitTyped(ic);
- }
+ commitTyped(ic);
maybeRemovePreviousPeriod(text);
ic.commitText(text, 1);
ic.endBatchEdit();
- updateShiftKeyState(getCurrentInputEditorInfo());
- mKeyboardSwitcher.onKey(0); // dummy key code.
- mJustRevertedSeparator = null;
- mJustAddedAutoSpace = false;
+ mKeyboardSwitcher.updateShiftState();
+ mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY);
+ mJustAddedMagicSpace = false;
mEnteredText = text;
}
- public void onCancel() {
+ @Override
+ public void onCancelInput() {
// User released a finger outside any key
mKeyboardSwitcher.onCancelInput();
}
- private void handleBackspace() {
- if (VOICE_INSTALLED && mVoiceInputHighlighted) {
- mVoiceInput.incrementTextModificationDeleteCount(
- mVoiceResults.candidates.get(0).toString().length());
- revertVoiceInput();
- return;
- }
- boolean deleteChar = false;
- InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
+ private void handleBackspace(boolean justReplacedDoubleSpace) {
+ if (mVoiceProxy.logAndRevertVoiceInput()) return;
+ final InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
ic.beginBatchEdit();
- 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);
- }
- }
+ mVoiceProxy.handleBackspace();
- if (mPredicting) {
+ boolean deleteChar = false;
+ if (mHasUncommittedTypedChars) {
final int length = mComposing.length();
if (length > 0) {
mComposing.delete(length - 1, length);
mWord.deleteLast();
ic.setComposingText(mComposing, 1);
if (mComposing.length() == 0) {
- mPredicting = false;
+ mHasUncommittedTypedChars = false;
+ }
+ if (1 == length) {
+ // 1 == length means we are about to erase the last character of the word,
+ // so we can show bigrams.
+ mHandler.postUpdateBigramPredictions();
+ } else {
+ // length > 1, so we still have letters to deduce a suggestion from.
+ mHandler.postUpdateSuggestions();
}
- postUpdateSuggestions();
} else {
ic.deleteSurroundingText(1, 0);
}
} else {
deleteChar = true;
}
- postUpdateShiftKeyState();
+ mHandler.postUpdateShiftKeyState();
+
TextEntryState.backspace();
- if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) {
+ if (TextEntryState.isUndoCommit()) {
revertLastWord(deleteChar);
ic.endBatchEdit();
return;
- } else if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
+ }
+ if (justReplacedDoubleSpace) {
+ if (revertDoubleSpace()) {
+ ic.endBatchEdit();
+ return;
+ }
+ }
+
+ if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
ic.deleteSurroundingText(mEnteredText.length(), 0);
} else if (deleteChar) {
if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
@@ -1345,178 +1229,176 @@ public class LatinIME extends InputMethodService
}
}
}
- mJustRevertedSeparator = null;
ic.endBatchEdit();
}
- private void resetShift() {
- handleShiftInternal(true);
- }
+ private void handleTab() {
+ final int imeOptions = getCurrentInputEditorInfo().imeOptions;
+ if (!EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
+ && !EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)) {
+ sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
+ return;
+ }
- private void handleShift() {
- handleShiftInternal(false);
- }
+ final InputConnection ic = getCurrentInputConnection();
+ if (ic == null)
+ return;
- private void handleShiftInternal(boolean forceNormal) {
- mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE);
- KeyboardSwitcher switcher = mKeyboardSwitcher;
- LatinKeyboardView inputView = switcher.getInputView();
- if (switcher.isAlphabetMode()) {
- if (mCapsLock || forceNormal) {
- mCapsLock = false;
- switcher.setShifted(false);
- } else if (inputView != null) {
- if (inputView.isShifted()) {
- mCapsLock = true;
- switcher.setShiftLocked(true);
- } else {
- switcher.setShifted(true);
- }
- }
- } else {
- switcher.toggleShift();
+ // True if keyboard is in either chording shift or manual temporary upper case mode.
+ final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase();
+ if (EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
+ && !isManualTemporaryUpperCase) {
+ EditorInfoCompatUtils.performEditorActionNext(ic);
+ } else if (EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)
+ && isManualTemporaryUpperCase) {
+ EditorInfoCompatUtils.performEditorActionPrevious(ic);
}
}
- private void abortCorrection(boolean force) {
- if (force || TextEntryState.isCorrecting()) {
- getCurrentInputConnection().finishComposingText();
- clearSuggestions();
- }
- }
+ private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) {
+ mVoiceProxy.handleCharacter();
- private void handleCharacter(int primaryCode, int[] keyCodes) {
- if (VOICE_INSTALLED && mVoiceInputHighlighted) {
- commitVoiceInput();
+ if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceStripper(primaryCode)) {
+ removeTrailingSpace();
}
- if (mAfterVoiceInput) {
- // Assume input length is 1. This assumption fails for smiley face insertions.
- mVoiceInput.incrementTextModificationInsertCount(1);
- }
- if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) {
- abortCorrection(false);
+ if (mLastSelectionStart == mLastSelectionEnd) {
+ mRecorrection.abortRecorrection(false);
}
- if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) {
- if (!mPredicting) {
- mPredicting = true;
+ int code = primaryCode;
+ if (isAlphabet(code) && isSuggestionsRequested() && !isCursorTouchingWord()) {
+ if (!mHasUncommittedTypedChars) {
+ mHasUncommittedTypedChars = true;
mComposing.setLength(0);
- saveWordInHistory(mBestWord);
+ mRecorrection.saveRecorrectionSuggestion(mWord, mBestWord);
mWord.reset();
+ clearSuggestions();
}
}
- if (mKeyboardSwitcher.getInputView().isShifted()) {
+ final KeyboardSwitcher switcher = mKeyboardSwitcher;
+ if (switcher.isShiftedOrShiftLocked()) {
if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
|| keyCodes[0] > Character.MAX_CODE_POINT) {
return;
}
- primaryCode = keyCodes[0];
- if (mKeyboardSwitcher.isAlphabetMode() && Character.isLowerCase(primaryCode)) {
+ code = keyCodes[0];
+ if (switcher.isAlphabetMode() && Character.isLowerCase(code)) {
// In some locales, such as Turkish, Character.toUpperCase() may return a wrong
// character because it doesn't take care of locale.
- final String upperCaseString = new String(new int[] {primaryCode}, 0, 1)
- .toUpperCase(mLanguageSwitcher.getInputLocale());
+ final String upperCaseString = new String(new int[] {code}, 0, 1)
+ .toUpperCase(mSubtypeSwitcher.getInputLocale());
if (upperCaseString.codePointCount(0, upperCaseString.length()) == 1) {
- primaryCode = upperCaseString.codePointAt(0);
+ code = upperCaseString.codePointAt(0);
} else {
// Some keys, such as [eszett], have upper case as multi-characters.
- onText(upperCaseString);
+ onTextInput(upperCaseString);
return;
}
}
}
- if (mPredicting) {
- if (mKeyboardSwitcher.getInputView().isShifted()
- && mKeyboardSwitcher.isAlphabetMode()
- && mComposing.length() == 0) {
+ if (mHasUncommittedTypedChars) {
+ if (mComposing.length() == 0 && switcher.isAlphabetMode()
+ && switcher.isShiftedOrShiftLocked()) {
mWord.setFirstCharCapitalized(true);
}
- mComposing.append((char) primaryCode);
- mWord.add(primaryCode, keyCodes);
+ mComposing.append((char) code);
+ mWord.add(code, keyCodes, x, y);
InputConnection ic = getCurrentInputConnection();
if (ic != null) {
// If it's the first letter, make note of auto-caps state
if (mWord.size() == 1) {
- mWord.setAutoCapitalized(
- getCursorCapsMode(ic, getCurrentInputEditorInfo()) != 0);
+ mWord.setAutoCapitalized(getCurrentAutoCapsState());
}
ic.setComposingText(mComposing, 1);
}
- postUpdateSuggestions();
+ mHandler.postUpdateSuggestions();
} else {
- sendKeyChar((char)primaryCode);
+ sendKeyChar((char)code);
+ }
+ if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
+ swapSwapperAndSpace();
+ } else {
+ mJustAddedMagicSpace = false;
}
- updateShiftKeyState(getCurrentInputEditorInfo());
+
+ switcher.updateShiftState();
if (LatinIME.PERF_DEBUG) measureCps();
- TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode));
+ TextEntryState.typedCharacter((char) code, mSettingsValues.isWordSeparator(code), x, y);
}
- private void handleSeparator(int primaryCode) {
- if (VOICE_INSTALLED && mVoiceInputHighlighted) {
- commitVoiceInput();
- }
-
- if (mAfterVoiceInput){
- // Assume input length is 1. This assumption fails for smiley face insertions.
- mVoiceInput.incrementTextModificationInsertPunctuationCount(1);
- }
+ private void handleSeparator(int primaryCode, int x, int y) {
+ mVoiceProxy.handleSeparator();
// Should dismiss the "Touch again to save" message when handling separator
if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
- postUpdateSuggestions();
+ mHandler.cancelUpdateBigramPredictions();
+ mHandler.postUpdateSuggestions();
}
boolean pickedDefault = false;
// Handle separator
- InputConnection ic = getCurrentInputConnection();
+ final InputConnection ic = getCurrentInputConnection();
if (ic != null) {
ic.beginBatchEdit();
- abortCorrection(false);
+ mRecorrection.abortRecorrection(false);
}
- if (mPredicting) {
+ if (mHasUncommittedTypedChars) {
// In certain languages where single quote is a separator, it's better
// not to auto correct, but accept the typed word. For instance,
// in Italian dov' should not be expanded to dove' because the elision
// requires the last vowel to be removed.
- if (mAutoCorrectOn && primaryCode != '\'' &&
- (mJustRevertedSeparator == null
- || mJustRevertedSeparator.length() == 0
- || mJustRevertedSeparator.charAt(0) != primaryCode)) {
- pickedDefault = pickDefaultSuggestion();
- // Picked the suggestion by the space key. We consider this
- // as "added an auto space".
- if (primaryCode == KEYCODE_SPACE) {
- mJustAddedAutoSpace = true;
- }
+ final boolean shouldAutoCorrect =
+ (mSettingsValues.mAutoCorrectEnabled || mSettingsValues.mQuickFixes)
+ && !mInputTypeNoAutoCorrect && mHasDictionary;
+ if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
+ pickedDefault = pickDefaultSuggestion(primaryCode);
} else {
commitTyped(ic);
}
}
- if (mJustAddedAutoSpace && primaryCode == KEYCODE_ENTER) {
- removeTrailingSpace();
- mJustAddedAutoSpace = false;
- }
- sendKeyChar((char)primaryCode);
- // Handle the case of ". ." -> " .." with auto-space if necessary
- // before changing the TextEntryState.
- if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED
- && primaryCode == KEYCODE_PERIOD) {
- reswapPeriodAndSpace();
+ if (mJustAddedMagicSpace) {
+ if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
+ sendKeyChar((char)primaryCode);
+ swapSwapperAndSpace();
+ } else {
+ if (mSettingsValues.isMagicSpaceStripper(primaryCode)) removeTrailingSpace();
+ sendKeyChar((char)primaryCode);
+ mJustAddedMagicSpace = false;
+ }
+ } else {
+ sendKeyChar((char)primaryCode);
}
- TextEntryState.typedCharacter((char) primaryCode, true);
- if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED
- && primaryCode != KEYCODE_ENTER) {
- swapPunctuationAndSpace();
- } else if (isPredictionOn() && primaryCode == KEYCODE_SPACE) {
- doubleSpace();
+ if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) {
+ maybeDoubleSpace();
}
+
+ TextEntryState.typedCharacter((char) primaryCode, true, x, y);
+
if (pickedDefault) {
- TextEntryState.backToAcceptedDefault(mWord.getTypedWord());
+ CharSequence typedWord = mWord.getTypedWord();
+ TextEntryState.backToAcceptedDefault(typedWord);
+ if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) {
+ InputConnectionCompatUtils.commitCorrection(
+ ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord);
+ if (mCandidateView != null)
+ mCandidateView.onAutoCorrectionInverted(mBestWord);
+ }
+ }
+ if (Keyboard.CODE_SPACE == primaryCode) {
+ if (!isCursorTouchingWord()) {
+ mHandler.cancelUpdateSuggestions();
+ mHandler.cancelUpdateOldSuggestions();
+ mHandler.postUpdateBigramPredictions();
+ }
+ } else {
+ // Set punctuation right away. onUpdateSelection will fire but tests whether it is
+ // already displayed or not, so it's okay.
+ setPunctuationSuggestions();
}
- updateShiftKeyState(getCurrentInputEditorInfo());
+ mKeyboardSwitcher.updateShiftState();
if (ic != null) {
ic.endBatchEdit();
}
@@ -1524,367 +1406,172 @@ public class LatinIME extends InputMethodService
private void handleClose() {
commitTyped(getCurrentInputConnection());
- if (VOICE_INSTALLED & mRecognizing) {
- mVoiceInput.cancel();
- }
+ mVoiceProxy.handleClose();
requestHideSelf(0);
- if (mKeyboardSwitcher != null) {
- LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
- if (inputView != null) {
- inputView.closing();
- }
- }
- TextEntryState.endSession();
- }
-
- private void saveWordInHistory(CharSequence result) {
- if (mWord.size() <= 1) {
- mWord.reset();
- 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();
- TypedWordAlternatives entry = new TypedWordAlternatives(resultCopy,
- new WordComposer(mWord));
- mWordHistory.add(entry);
+ LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
+ if (inputView != null)
+ inputView.closing();
}
- private void postUpdateSuggestions() {
- mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS);
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100);
+ public boolean isSuggestionsRequested() {
+ return mIsSettingsSuggestionStripOn
+ && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
}
- private void postUpdateOldSuggestions() {
- mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), 300);
+ public boolean isShowingPunctuationList() {
+ return mSettingsValues.mSuggestPuncList == mCandidateView.getSuggestions();
}
- private boolean isPredictionOn() {
- return mPredictionOn;
+ public boolean isShowingSuggestionsStrip() {
+ return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
+ || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
+ && mOrientation == Configuration.ORIENTATION_PORTRAIT);
}
- private boolean isCandidateStripVisible() {
- return isPredictionOn() && mShowSuggestions;
- }
-
- public void onCancelVoice() {
- if (mRecognizing) {
- switchToKeyboardView();
- }
- }
-
- private void switchToKeyboardView() {
- mHandler.post(new Runnable() {
- public void run() {
- mRecognizing = false;
- if (mKeyboardSwitcher.getInputView() != null) {
- setInputView(mKeyboardSwitcher.getInputView());
- }
- setCandidatesViewShown(true);
- updateInputViewShown();
- postUpdateSuggestions();
- }});
- }
-
- private void switchToRecognitionStatusView() {
- final boolean configChanged = mConfigurationChanging;
- mHandler.post(new Runnable() {
- public void run() {
- setCandidatesViewShown(false);
- mRecognizing = true;
- View v = mVoiceInput.getView();
- ViewParent p = v.getParent();
- if (p != null && p instanceof ViewGroup) {
- ((ViewGroup)v.getParent()).removeView(v);
- }
- setInputView(v);
- updateInputViewShown();
- if (configChanged) {
- mVoiceInput.onConfigurationChanged();
- }
- }});
- }
-
- private void startListening(boolean swipe) {
- if (!mHasUsedVoiceInput ||
- (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale)) {
- // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel.
- showVoiceWarningDialog(swipe);
- } else {
- reallyStartListening(swipe);
- }
- }
-
- private void reallyStartListening(boolean swipe) {
- 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(this).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(this).edit();
- editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true);
- SharedPreferencesCompat.apply(editor);
- mHasUsedVoiceInputUnsupportedLocale = true;
- }
-
- // Clear N-best suggestions
- clearSuggestions();
-
- FieldContext context = new FieldContext(
- getCurrentInputConnection(),
- getCurrentInputEditorInfo(),
- mLanguageSwitcher.getInputLanguage(),
- mLanguageSwitcher.getEnabledLanguages());
- mVoiceInput.startListening(context, swipe);
- switchToRecognitionStatusView();
- }
-
- private void showVoiceWarningDialog(final boolean swipe) {
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setCancelable(true);
- builder.setIcon(R.drawable.ic_mic_dialog);
- builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int whichButton) {
- mVoiceInput.logKeyboardWarningDialogOk();
- reallyStartListening(swipe);
- }
- });
- builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int whichButton) {
- mVoiceInput.logKeyboardWarningDialogCancel();
- }
- });
-
- if (mLocaleSupportedForVoiceInput) {
- String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" +
- getString(R.string.voice_warning_how_to_turn_off);
- builder.setMessage(message);
- } else {
- String message = getString(R.string.voice_warning_locale_not_supported) + "\n\n" +
- getString(R.string.voice_warning_may_not_understand) + "\n\n" +
- getString(R.string.voice_warning_how_to_turn_off);
- builder.setMessage(message);
- }
-
- builder.setTitle(R.string.voice_warning_title);
- mVoiceWarningDialog = builder.create();
-
- Window window = mVoiceWarningDialog.getWindow();
- WindowManager.LayoutParams lp = window.getAttributes();
- lp.token = mKeyboardSwitcher.getInputView().getWindowToken();
- lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
- window.setAttributes(lp);
- window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
- mVoiceInput.logKeyboardWarningDialogShown();
- mVoiceWarningDialog.show();
- }
-
- public void onVoiceResults(List<String> candidates,
- Map<String, List<CharSequence>> alternatives) {
- if (!mRecognizing) {
- return;
- }
- mVoiceResults.candidates = candidates;
- mVoiceResults.alternatives = alternatives;
- mHandler.sendMessage(mHandler.obtainMessage(MSG_VOICE_RESULTS));
+ public boolean isCandidateStripVisible() {
+ if (mCandidateView == null)
+ return false;
+ if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting())
+ return true;
+ if (!isShowingSuggestionsStrip())
+ return false;
+ if (mApplicationSpecifiedCompletionOn)
+ return true;
+ return isSuggestionsRequested();
}
- private void handleVoiceResults() {
- mAfterVoiceInput = true;
- mImmediatelyAfterVoiceInput = true;
-
- InputConnection ic = getCurrentInputConnection();
- if (!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);
- }
- }
-
- vibrate();
- switchToKeyboardView();
-
- final List<CharSequence> nBest = new ArrayList<CharSequence>();
- boolean capitalizeFirstWord = preferCapitalization()
- || (mKeyboardSwitcher.isAlphabetMode()
- && mKeyboardSwitcher.getInputView().isShifted());
- for (String c : mVoiceResults.candidates) {
- if (capitalizeFirstWord) {
- c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length());
+ public void switchToKeyboardView() {
+ if (DEBUG) {
+ Log.d(TAG, "Switch to keyboard view.");
+ }
+ View v = mKeyboardSwitcher.getKeyboardView();
+ if (v != null) {
+ // Confirms that the keyboard view doesn't have parent view.
+ ViewParent p = v.getParent();
+ if (p != null && p instanceof ViewGroup) {
+ ((ViewGroup) p).removeView(v);
}
- nBest.add(c);
+ setInputView(v);
}
-
- 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
-
- commitTyped(ic);
- EditingUtil.appendText(ic, bestResult);
-
- if (ic != null) ic.endBatchEdit();
-
- mVoiceInputHighlighted = true;
- mWordToSuggestions.putAll(mVoiceResults.alternatives);
+ setSuggestionStripShown(isCandidateStripVisible());
+ updateInputViewShown();
+ mHandler.postUpdateSuggestions();
}
- private void clearSuggestions() {
- setSuggestions(null, false, false, false);
+ public void clearSuggestions() {
+ setSuggestions(SuggestedWords.EMPTY);
}
- private void setSuggestions(
- List<CharSequence> suggestions,
- boolean completions,
- boolean typedWordValid,
- boolean haveMinimalSuggestion) {
-
- if (mIsShowingHint) {
- setCandidatesView(mCandidateViewContainer);
- mIsShowingHint = false;
- }
-
+ public void setSuggestions(SuggestedWords words) {
if (mCandidateView != null) {
- mCandidateView.setSuggestions(
- suggestions, completions, typedWordValid, haveMinimalSuggestion);
+ mCandidateView.setSuggestions(words);
+ mKeyboardSwitcher.onAutoCorrectionStateChanged(
+ words.hasWordAboveAutoCorrectionScoreThreshold());
}
}
- private void updateSuggestions() {
- LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
- ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null);
-
+ public void updateSuggestions() {
// Check if we have a suggestion engine attached.
- if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) {
+ if ((mSuggest == null || !isSuggestionsRequested())
+ && !mVoiceProxy.isVoiceInputHighlighted()) {
return;
}
- if (!mPredicting) {
- setNextSuggestions();
+ if (!mHasUncommittedTypedChars) {
+ setPunctuationSuggestions();
return;
}
showSuggestions(mWord);
}
- private List<CharSequence> getTypedSuggestions(WordComposer word) {
- List<CharSequence> stringList = mSuggest.getSuggestions(
- mKeyboardSwitcher.getInputView(), word, false, null);
- return stringList;
- }
-
- private void showCorrections(WordAlternatives alternatives) {
- List<CharSequence> stringList = alternatives.getAlternatives();
- ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(null);
- showSuggestions(stringList, alternatives.getOriginalWord(), false, false);
- }
-
private void showSuggestions(WordComposer word) {
- // long startTime = System.currentTimeMillis(); // TIME MEASUREMENT!
- // TODO Maybe need better way of retrieving previous word
- CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(),
- mWordSeparators);
- List<CharSequence> stringList = mSuggest.getSuggestions(
- mKeyboardSwitcher.getInputView(), word, false, prevWord);
- // long stopTime = System.currentTimeMillis(); // TIME MEASUREMENT!
- // Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime));
-
- int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies();
-
- ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(
- nextLettersFrequencies);
-
- boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasMinimalCorrection();
- //|| mCorrectionMode == mSuggest.CORRECTION_FULL;
- CharSequence typedWord = word.getTypedWord();
- // If we're in basic correct
- boolean typedWordValid = mSuggest.isValidWord(typedWord) ||
- (preferCapitalization()
- && mSuggest.isValidWord(typedWord.toString().toLowerCase()));
+ // TODO: May need a better way of retrieving previous word
+ CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(),
+ mSettingsValues.mWordSeparators);
+ SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
+ mKeyboardSwitcher.getKeyboardView(), word, prevWord);
+
+ boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection();
+ final CharSequence typedWord = word.getTypedWord();
+ // Here, we want to promote a whitelisted word if exists.
+ final boolean typedWordValid = AutoCorrection.isValidWordForAutoCorrection(
+ mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization());
if (mCorrectionMode == Suggest.CORRECTION_FULL
|| mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
correctionAvailable |= typedWordValid;
}
// Don't auto-correct words with multiple capital letter
correctionAvailable &= !word.isMostlyCaps();
- correctionAvailable &= !TextEntryState.isCorrecting();
-
- showSuggestions(stringList, typedWord, typedWordValid, correctionAvailable);
+ correctionAvailable &= !TextEntryState.isRecorrecting();
+
+ // Basically, we update the suggestion strip only when suggestion count > 1. However,
+ // there is an exception: We update the suggestion strip whenever typed word's length
+ // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually,
+ // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
+ // need to clear the previous state when the user starts typing a word (i.e. typed word's
+ // length == 1).
+ if (typedWord != null) {
+ if (builder.size() > 1 || typedWord.length() == 1 || typedWordValid
+ || mCandidateView.isShowingAddToDictionaryHint()) {
+ builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(
+ correctionAvailable);
+ } else {
+ final SuggestedWords previousSuggestions = mCandidateView.getSuggestions();
+ if (previousSuggestions == mSettingsValues.mSuggestPuncList)
+ return;
+ builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions);
+ }
+ }
+ showSuggestions(builder.build(), typedWord);
}
- private void showSuggestions(List<CharSequence> stringList, CharSequence typedWord,
- boolean typedWordValid, boolean correctionAvailable) {
- setSuggestions(stringList, false, typedWordValid, correctionAvailable);
- if (stringList.size() > 0) {
- if (correctionAvailable && !typedWordValid && stringList.size() > 1) {
- mBestWord = stringList.get(1);
+ public void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) {
+ setSuggestions(suggestedWords);
+ if (suggestedWords.size() > 0) {
+ if (Utils.shouldBlockedBySafetyNetForAutoCorrection(suggestedWords, mSuggest)) {
+ mBestWord = typedWord;
+ } else if (suggestedWords.hasAutoCorrectionWord()) {
+ mBestWord = suggestedWords.getWord(1);
} else {
mBestWord = typedWord;
}
} else {
mBestWord = null;
}
- setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn);
+ setSuggestionStripShown(isCandidateStripVisible());
}
- private boolean pickDefaultSuggestion() {
+ private boolean pickDefaultSuggestion(int separatorCode) {
// Complete any pending candidate query first
- if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) {
- mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS);
+ if (mHandler.hasPendingUpdateSuggestions()) {
+ mHandler.cancelUpdateSuggestions();
updateSuggestions();
}
if (mBestWord != null && mBestWord.length() > 0) {
- TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord);
- mJustAccepted = true;
- pickSuggestion(mBestWord, false);
+ TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord, separatorCode);
+ mExpectingUpdateSelection = true;
+ commitBestWord(mBestWord);
// Add the word to the auto dictionary if it's not a known word
- addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED);
+ addToAutoAndUserBigramDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED);
return true;
-
}
return false;
}
+ @Override
public void pickSuggestionManually(int index, CharSequence suggestion) {
- List<CharSequence> suggestions = mCandidateView.getSuggestions();
-
- if (mAfterVoiceInput && mShowingVoiceSuggestions) {
- mVoiceInput.flushAllTextModificationCounters();
- // send this intent AFTER logging any prior aggregated edits.
- mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index,
- mWordSeparators,
- getCurrentInputConnection());
- }
+ SuggestedWords suggestions = mCandidateView.getSuggestions();
+ mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion,
+ mSettingsValues.mWordSeparators);
- final boolean correcting = TextEntryState.isCorrecting();
+ final boolean recorrecting = TextEntryState.isRecorrecting();
InputConnection ic = getCurrentInputConnection();
if (ic != null) {
ic.beginBatchEdit();
}
- if (mCompletionOn && mCompletions != null && index >= 0
- && index < mCompletions.length) {
- CompletionInfo ci = mCompletions[index];
+ if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null
+ && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
+ CompletionInfo ci = mApplicationSpecifiedCompletions[index];
if (ic != null) {
ic.commitCompletion(ci);
}
@@ -1892,7 +1579,7 @@ public class LatinIME extends InputMethodService
if (mCandidateView != null) {
mCandidateView.clear();
}
- updateShiftKeyState(getCurrentInputEditorInfo());
+ mKeyboardSwitcher.updateShiftState();
if (ic != null) {
ic.endBatchEdit();
}
@@ -1900,52 +1587,81 @@ public class LatinIME extends InputMethodService
}
// If this is a punctuation, apply it through the normal key press
- if (suggestion.length() == 1 && (isWordSeparator(suggestion.charAt(0))
- || isSuggestedPunctuation(suggestion.charAt(0)))) {
+ if (suggestion.length() == 1 && (mSettingsValues.isWordSeparator(suggestion.charAt(0))
+ || mSettingsValues.isSuggestedPunctuation(suggestion.charAt(0)))) {
// Word separators are suggested before the user inputs something.
// So, LatinImeLogger logs "" as a user's input.
LatinImeLogger.logOnManualSuggestion(
- "", suggestion.toString(), index, suggestions);
+ "", suggestion.toString(), index, suggestions.mWords);
+ // Find out whether the previous character is a space. If it is, as a special case
+ // for punctuation entered through the suggestion strip, it should be considered
+ // a magic space even if it was a normal space. This is meant to help in case the user
+ // pressed space on purpose of displaying the suggestion strip punctuation.
final char primaryCode = suggestion.charAt(0);
- onKey(primaryCode, new int[]{primaryCode}, LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE,
- LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE);
+ final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : "";
+ final int toLeft = (ic == null || TextUtils.isEmpty(beforeText))
+ ? 0 : beforeText.charAt(0);
+ final boolean oldMagicSpace = mJustAddedMagicSpace;
+ if (Keyboard.CODE_SPACE == toLeft) mJustAddedMagicSpace = true;
+ onCodeInput(primaryCode, new int[] { primaryCode },
+ KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
+ KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+ mJustAddedMagicSpace = oldMagicSpace;
if (ic != null) {
ic.endBatchEdit();
}
return;
}
- mJustAccepted = true;
- pickSuggestion(suggestion, correcting);
+ if (!mHasUncommittedTypedChars) {
+ // If we are not composing a word, then it was a suggestion inferred from
+ // context - no user input. We should reset the word composer.
+ mWord.reset();
+ }
+ mExpectingUpdateSelection = true;
+ commitBestWord(suggestion);
// Add the word to the auto dictionary if it's not a known word
if (index == 0) {
- addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED);
+ addToAutoAndUserBigramDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED);
} else {
- addToBigramDictionary(suggestion, 1);
+ addToOnlyBigramDictionary(suggestion, 1);
}
LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(),
- index, suggestions);
+ index, suggestions.mWords);
TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion);
// Follow it with a space
- if (mAutoSpace && !correcting) {
- sendSpace();
- mJustAddedAutoSpace = true;
- }
-
- final boolean showingAddToDictionaryHint = index == 0 && mCorrectionMode > 0
- && !mSuggest.isValidWord(suggestion)
- && !mSuggest.isValidWord(suggestion.toString().toLowerCase());
-
- if (!correcting) {
+ if (mShouldInsertMagicSpace && !recorrecting) {
+ sendMagicSpace();
+ }
+
+ // We should show the hint if the user pressed the first entry AND either:
+ // - There is no dictionary (we know that because we tried to load it => null != mSuggest
+ // AND mHasDictionary is false)
+ // - There is a dictionary and the word is not in it
+ // Please note that if mSuggest is null, it means that everything is off: suggestion
+ // and correction, so we shouldn't try to show the hint
+ // We used to look at mCorrectionMode here, but showing the hint should have nothing
+ // to do with the autocorrection setting.
+ final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
+ // If there is no dictionary the hint should be shown.
+ && (!mHasDictionary
+ // If "suggestion" is not in the dictionary, the hint should be shown.
+ || !AutoCorrection.isValidWord(
+ mSuggest.getUnigramDictionaries(), suggestion, true));
+
+ if (!recorrecting) {
// Fool the state watcher so that a subsequent backspace will not do a revert, unless
// we just did a correction, in which case we need to stay in
// TextEntryState.State.PICKED_SUGGESTION state.
- TextEntryState.typedCharacter((char) KEYCODE_SPACE, true);
- setNextSuggestions();
- } else if (!showingAddToDictionaryHint) {
+ TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true,
+ WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
+ }
+ if (!showingAddToDictionaryHint) {
// If we're not showing the "Touch again to save", then show corrections again.
// In case the cursor position doesn't change, make sure we show the suggestions again.
- clearSuggestions();
- postUpdateOldSuggestions();
+ updateBigramPredictions();
+ // Updating the predictions right away may be slow and feel unresponsive on slower
+ // terminals. On the other hand if we just postUpdateBigramPredictions() it will
+ // take a noticeable delay to update them which may feel uneasy.
}
if (showingAddToDictionaryHint) {
mCandidateView.showAddToDictionaryHint(suggestion);
@@ -1955,193 +1671,71 @@ public class LatinIME extends InputMethodService
}
}
- private void rememberReplacedWord(CharSequence suggestion) {
- if (mShowingVoiceSuggestions) {
- // Retain the replaced word in the alternatives array.
- EditingUtil.Range range = new EditingUtil.Range();
- String wordToBeReplaced = EditingUtil.getWordAtCursor(getCurrentInputConnection(),
- mWordSeparators, range);
- 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);
- }
- }
- }
-
/**
* Commits the chosen word to the text field and saves it for later
* retrieval.
- * @param suggestion the suggestion picked by the user to be committed to
- * the text field
- * @param correcting whether this is due to a correction of an existing
- * word.
*/
- private void pickSuggestion(CharSequence suggestion, boolean correcting) {
- final LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
- final Locale inputLocale = mLanguageSwitcher.getInputLocale();
- if (mCapsLock) {
- suggestion = suggestion.toString().toUpperCase(inputLocale);
- } else if (preferCapitalization()
- || (mKeyboardSwitcher.isAlphabetMode()
- && inputView.isShifted())) {
- suggestion = suggestion.toString().toUpperCase(inputLocale).charAt(0)
- + suggestion.subSequence(1, suggestion.length()).toString();
- }
+ private void commitBestWord(CharSequence bestWord) {
+ KeyboardSwitcher switcher = mKeyboardSwitcher;
+ if (!switcher.isKeyboardAvailable())
+ return;
InputConnection ic = getCurrentInputConnection();
if (ic != null) {
- rememberReplacedWord(suggestion);
- ic.commitText(suggestion, 1);
- }
- saveWordInHistory(suggestion);
- mPredicting = false;
- mCommittedLength = suggestion.length();
- ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null);
- // If we just corrected a word, then don't show punctuations
- if (!correcting) {
- setNextSuggestions();
- }
- updateShiftKeyState(getCurrentInputEditorInfo());
- }
-
- /**
- * 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.
- */
- private boolean applyVoiceAlternatives(EditingUtil.SelectedWord touching) {
- // Search for result in spoken word alternatives
- String selectedWord = touching.word.toString().trim();
- if (!mWordToSuggestions.containsKey(selectedWord)) {
- selectedWord = selectedWord.toLowerCase();
- }
- if (mWordToSuggestions.containsKey(selectedWord)) {
- mShowingVoiceSuggestions = true;
- List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
- // If the first letter of touching is capitalized, make all the suggestions
- // start with a capital letter.
- if (Character.isUpperCase(touching.word.charAt(0))) {
- final Locale inputLocale = mLanguageSwitcher.getInputLocale();
- for (int i = 0; i < suggestions.size(); i++) {
- String origSugg = (String) suggestions.get(i);
- String capsSugg = origSugg.toUpperCase(inputLocale).charAt(0)
- + origSugg.subSequence(1, origSugg.length()).toString();
- suggestions.set(i, capsSugg);
- }
- }
- setSuggestions(suggestions, false, true, true);
- setCandidatesViewShown(true);
- return true;
+ mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators);
+ SuggestedWords suggestedWords = mCandidateView.getSuggestions();
+ ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
+ this, bestWord, suggestedWords), 1);
}
- return false;
+ mRecorrection.saveRecorrectionSuggestion(mWord, bestWord);
+ mHasUncommittedTypedChars = false;
+ mCommittedLength = bestWord.length();
}
- /**
- * 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.
- */
- private boolean applyTypedAlternatives(EditingUtil.SelectedWord touching) {
- // If we didn't find a match, search for result in typed word history
- WordComposer foundWord = null;
- WordAlternatives alternatives = null;
- for (WordAlternatives entry : mWordHistory) {
- if (TextUtils.equals(entry.getChosenWord(), touching.word)) {
- if (entry instanceof TypedWordAlternatives) {
- foundWord = ((TypedWordAlternatives) entry).word;
- }
- alternatives = entry;
- break;
- }
- }
- // If we didn't find a match, at least suggest completions
- if (foundWord == null
- && (mSuggest.isValidWord(touching.word)
- || mSuggest.isValidWord(touching.word.toString().toLowerCase()))) {
- foundWord = new WordComposer();
- for (int i = 0; i < touching.word.length(); i++) {
- foundWord.add(touching.word.charAt(i), new int[] {
- touching.word.charAt(i)
- });
- }
- foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.word.charAt(0)));
- }
- // Found a match, show suggestions
- if (foundWord != null || alternatives != null) {
- if (alternatives == null) {
- alternatives = new TypedWordAlternatives(touching.word, foundWord);
- }
- showCorrections(alternatives);
- if (foundWord != null) {
- mWord = new WordComposer(foundWord);
- } else {
- mWord.reset();
- }
- return true;
- }
- return false;
- }
+ private static final WordComposer sEmptyWordComposer = new WordComposer();
+ public void updateBigramPredictions() {
+ if (mSuggest == null || !isSuggestionsRequested())
+ return;
- private void setOldSuggestions() {
- mShowingVoiceSuggestions = false;
- if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) {
+ if (!mSettingsValues.mBigramPredictionEnabled) {
+ setPunctuationSuggestions();
return;
}
- InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- if (!mPredicting) {
- // Extract the selected or touching text
- EditingUtil.SelectedWord touching = EditingUtil.getWordAtCursorOrSelection(ic,
- mLastSelectionStart, mLastSelectionEnd, mWordSeparators);
- if (touching != null && touching.word.length() > 1) {
- ic.beginBatchEdit();
+ final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
+ mSettingsValues.mWordSeparators);
+ SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
+ mKeyboardSwitcher.getKeyboardView(), sEmptyWordComposer, prevWord);
- if (!applyVoiceAlternatives(touching) && !applyTypedAlternatives(touching)) {
- abortCorrection(true);
- } else {
- TextEntryState.selectedForCorrection();
- EditingUtil.underlineWord(ic, touching);
- }
-
- ic.endBatchEdit();
- } else {
- abortCorrection(true);
- setNextSuggestions(); // Show the punctuation suggestions list
- }
+ if (builder.size() > 0) {
+ // Explicitly supply an empty typed word (the no-second-arg version of
+ // showSuggestions will retrieve the word near the cursor, we don't want that here)
+ showSuggestions(builder.build(), "");
} else {
- abortCorrection(true);
+ if (!isShowingPunctuationList()) setPunctuationSuggestions();
}
}
- private void setNextSuggestions() {
- setSuggestions(mSuggestPuncList, false, false, false);
+ public void setPunctuationSuggestions() {
+ setSuggestions(mSettingsValues.mSuggestPuncList);
+ setSuggestionStripShown(isCandidateStripVisible());
}
- private void addToDictionaries(CharSequence suggestion, int frequencyDelta) {
+ private void addToAutoAndUserBigramDictionaries(CharSequence suggestion, int frequencyDelta) {
checkAddToDictionary(suggestion, frequencyDelta, false);
}
- private void addToBigramDictionary(CharSequence suggestion, int frequencyDelta) {
+ private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) {
checkAddToDictionary(suggestion, frequencyDelta, true);
}
/**
* Adds to the UserBigramDictionary and/or AutoDictionary
- * @param addToBigramDictionary true if it should be added to bigram dictionary if possible
+ * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible
*/
private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta,
- boolean addToBigramDictionary) {
+ boolean selectedANotTypedWord) {
if (suggestion == null || suggestion.length() < 1) return;
+
// Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
// adding words in situations where the user or application really didn't
// want corrections enabled or learned.
@@ -2149,36 +1743,42 @@ public class LatinIME extends InputMethodService
|| mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
return;
}
- if (suggestion != null) {
- if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion)
- || (!mSuggest.isValidWord(suggestion.toString())
- && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) {
- mAutoDictionary.addWord(suggestion.toString(), frequencyDelta);
- }
- if (mUserBigramDictionary != null) {
- CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(),
- mSentenceSeparators);
- if (!TextUtils.isEmpty(prevWord)) {
- mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
- }
+ final boolean selectedATypedWordAndItsInAutoDic =
+ !selectedANotTypedWord && mAutoDictionary.isValidWord(suggestion);
+ final boolean isValidWord = AutoCorrection.isValidWord(
+ mSuggest.getUnigramDictionaries(), suggestion, true);
+ final boolean needsToAddToAutoDictionary = selectedATypedWordAndItsInAutoDic
+ || !isValidWord;
+ if (needsToAddToAutoDictionary) {
+ mAutoDictionary.addWord(suggestion.toString(), frequencyDelta);
+ }
+
+ if (mUserBigramDictionary != null) {
+ // We don't want to register as bigrams words separated by a separator.
+ // For example "I will, and you too" : we don't want the pair ("will" "and") to be
+ // a bigram.
+ CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(),
+ mSettingsValues.mWordSeparators);
+ if (!TextUtils.isEmpty(prevWord)) {
+ mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
}
}
}
- private boolean isCursorTouchingWord() {
+ public boolean isCursorTouchingWord() {
InputConnection ic = getCurrentInputConnection();
if (ic == null) return false;
CharSequence toLeft = ic.getTextBeforeCursor(1, 0);
CharSequence toRight = ic.getTextAfterCursor(1, 0);
if (!TextUtils.isEmpty(toLeft)
- && !isWordSeparator(toLeft.charAt(0))
- && !isSuggestedPunctuation(toLeft.charAt(0))) {
+ && !mSettingsValues.isWordSeparator(toLeft.charAt(0))
+ && !mSettingsValues.isSuggestedPunctuation(toLeft.charAt(0))) {
return true;
}
if (!TextUtils.isEmpty(toRight)
- && !isWordSeparator(toRight.charAt(0))
- && !isSuggestedPunctuation(toRight.charAt(0))) {
+ && !mSettingsValues.isWordSeparator(toRight.charAt(0))
+ && !mSettingsValues.isSuggestedPunctuation(toRight.charAt(0))) {
return true;
}
return false;
@@ -2191,165 +1791,140 @@ public class LatinIME extends InputMethodService
public void revertLastWord(boolean deleteChar) {
final int length = mComposing.length();
- if (!mPredicting && length > 0) {
+ if (!mHasUncommittedTypedChars && length > 0) {
final InputConnection ic = getCurrentInputConnection();
- mPredicting = true;
- mJustRevertedSeparator = ic.getTextBeforeCursor(1, 0);
+ final CharSequence punctuation = ic.getTextBeforeCursor(1, 0);
if (deleteChar) ic.deleteSurroundingText(1, 0);
int toDelete = mCommittedLength;
- CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0);
- if (toTheLeft != null && toTheLeft.length() > 0
- && isWordSeparator(toTheLeft.charAt(0))) {
+ final CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0);
+ if (!TextUtils.isEmpty(toTheLeft)
+ && mSettingsValues.isWordSeparator(toTheLeft.charAt(0))) {
toDelete--;
}
ic.deleteSurroundingText(toDelete, 0);
- ic.setComposingText(mComposing, 1);
- TextEntryState.backspace();
- postUpdateSuggestions();
+ // Re-insert punctuation only when the deleted character was word separator and the
+ // composing text wasn't equal to the auto-corrected text.
+ if (deleteChar
+ && !TextUtils.isEmpty(punctuation)
+ && mSettingsValues.isWordSeparator(punctuation.charAt(0))
+ && !TextUtils.equals(mComposing, toTheLeft)) {
+ ic.commitText(mComposing, 1);
+ TextEntryState.acceptedTyped(mComposing);
+ ic.commitText(punctuation, 1);
+ TextEntryState.typedCharacter(punctuation.charAt(0), true,
+ WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
+ // Clear composing text
+ mComposing.setLength(0);
+ } else {
+ mHasUncommittedTypedChars = true;
+ ic.setComposingText(mComposing, 1);
+ TextEntryState.backspace();
+ }
+ mHandler.cancelUpdateBigramPredictions();
+ mHandler.postUpdateSuggestions();
} else {
sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
- mJustRevertedSeparator = null;
}
}
- protected String getWordSeparators() {
- return mWordSeparators;
+ public boolean revertDoubleSpace() {
+ mHandler.cancelDoubleSpacesTimer();
+ final InputConnection ic = getCurrentInputConnection();
+ // Here we test whether we indeed have a period and a space before us. This should not
+ // be needed, but it's there just in case something went wrong.
+ final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
+ if (!". ".equals(textBeforeCursor))
+ return false;
+ ic.beginBatchEdit();
+ ic.deleteSurroundingText(2, 0);
+ ic.commitText(" ", 1);
+ ic.endBatchEdit();
+ return true;
}
public boolean isWordSeparator(int code) {
- String separators = getWordSeparators();
- return separators.contains(String.valueOf((char)code));
- }
-
- private boolean isSentenceSeparator(int code) {
- return mSentenceSeparators.contains(String.valueOf((char)code));
+ return mSettingsValues.isWordSeparator(code);
}
- private void sendSpace() {
- sendKeyChar((char)KEYCODE_SPACE);
- updateShiftKeyState(getCurrentInputEditorInfo());
- //onKey(KEY_SPACE[0], KEY_SPACE);
+ private void sendMagicSpace() {
+ sendKeyChar((char)Keyboard.CODE_SPACE);
+ mJustAddedMagicSpace = true;
+ mKeyboardSwitcher.updateShiftState();
}
public boolean preferCapitalization() {
return mWord.isFirstCharCapitalized();
}
- private void toggleLanguage(boolean reset, boolean next) {
- if (reset) {
- mLanguageSwitcher.reset();
- } else {
- if (next) {
- mLanguageSwitcher.next();
- } else {
- mLanguageSwitcher.prev();
- }
- }
- int currentKeyboardMode = mKeyboardSwitcher.getKeyboardMode();
- reloadKeyboards();
- mKeyboardSwitcher.makeKeyboards(true);
- mKeyboardSwitcher.setKeyboardMode(currentKeyboardMode, 0,
- mEnableVoiceButton && mEnableVoice);
- initSuggest(mLanguageSwitcher.getInputLanguage());
- mLanguageSwitcher.persist();
- updateShiftKeyState(getCurrentInputEditorInfo());
- }
-
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
- String key) {
- if (PREF_SELECTED_LANGUAGES.equals(key)) {
- mLanguageSwitcher.loadLocales(sharedPreferences);
- mRefreshKeyboardRequired = true;
- } else if (PREF_RECORRECTION_ENABLED.equals(key)) {
- mReCorrectionEnabled = sharedPreferences.getBoolean(PREF_RECORRECTION_ENABLED,
- getResources().getBoolean(R.bool.default_recorrection_enabled));
- }
- }
-
- public void swipeRight() {
- if (LatinKeyboardView.DEBUG_AUTO_PLAY) {
- ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE));
- CharSequence text = cm.getText();
- if (!TextUtils.isEmpty(text)) {
- mKeyboardSwitcher.getInputView().startPlaying(text.toString());
- }
- }
- }
-
- public void swipeLeft() {
+ // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
+ // according to new language or mode.
+ public void onRefreshKeyboard() {
+ // Reload keyboard because the current language has been changed.
+ mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(),
+ mSubtypeSwitcher.isShortcutImeEnabled() && mVoiceProxy.isVoiceButtonEnabled(),
+ mVoiceProxy.isVoiceButtonOnPrimary());
+ initSuggest();
+ loadSettings();
+ mKeyboardSwitcher.updateShiftState();
}
- public void swipeDown() {
- handleClose();
- }
+ // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER.
+ private void toggleLanguage(boolean next) {
+ if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()) {
+ mSubtypeSwitcher.toggleLanguage(next);
+ }
+ // The following is necessary because on API levels < 10, we don't get notified when
+ // subtype changes.
+ if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED)
+ onRefreshKeyboard();
+ }
- public void swipeUp() {
- //launchSettings();
+ @Override
+ public void onSwipeDown() {
+ if (mSettingsValues.mSwipeDownDismissKeyboardEnabled)
+ handleClose();
}
- public void onPress(int primaryCode) {
+ @Override
+ public void onPress(int primaryCode, boolean withSliding) {
if (mKeyboardSwitcher.isVibrateAndSoundFeedbackRequired()) {
vibrate();
playKeyClick(primaryCode);
}
- final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch();
- if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_SHIFT) {
- mShiftKeyState.onPress();
- handleShift();
- } else if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_MODE_CHANGE) {
- changeKeyboardMode();
- mSymbolKeyState.onPress();
- mKeyboardSwitcher.setAutoModeSwitchStateMomentary();
+ KeyboardSwitcher switcher = mKeyboardSwitcher;
+ final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
+ if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
+ switcher.onPressShift(withSliding);
+ } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
+ switcher.onPressSymbol();
} else {
- mShiftKeyState.onOtherKeyPressed();
- mSymbolKeyState.onOtherKeyPressed();
+ switcher.onOtherKeyPressed();
}
}
- public void onRelease(int primaryCode) {
+ @Override
+ public void onRelease(int primaryCode, boolean withSliding) {
+ KeyboardSwitcher switcher = mKeyboardSwitcher;
// Reset any drag flags in the keyboard
- ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).keyReleased();
- //vibrate();
- final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch();
- if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_SHIFT) {
- if (mShiftKeyState.isMomentary())
- resetShift();
- mShiftKeyState.onRelease();
- } else if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_MODE_CHANGE) {
- // Snap back to the previous keyboard mode if the user chords the mode change key and
- // other key, then released the mode change key.
- if (mKeyboardSwitcher.isInChordingAutoModeSwitchState())
- changeKeyboardMode();
- mSymbolKeyState.onRelease();
+ final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
+ if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
+ switcher.onReleaseShift(withSliding);
+ } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
+ switcher.onReleaseSymbol();
}
}
- private FieldContext makeFieldContext() {
- return new FieldContext(
- getCurrentInputConnection(),
- getCurrentInputEditorInfo(),
- mLanguageSwitcher.getInputLanguage(),
- mLanguageSwitcher.getEnabledLanguages());
- }
-
- private boolean fieldCanDoVoice(FieldContext fieldContext) {
- return !mPasswordText
- && mVoiceInput != null
- && !mVoiceInput.isBlacklistedField(fieldContext);
- }
-
- private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) {
- return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext)
- && !(attribute != null
- && IME_OPTION_NO_MICROPHONE.equals(attribute.privateImeOptions))
- && SpeechRecognizer.isRecognitionAvailable(this);
- }
- // receive ringer mode changes to detect silent mode
+ // receive ringer mode change and network state change.
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- updateRingerMode();
+ final String action = intent.getAction();
+ if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
+ updateRingerMode();
+ } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ mSubtypeSwitcher.onNetworkStateChanged(intent);
+ }
}
};
@@ -2359,7 +1934,7 @@ public class LatinIME extends InputMethodService
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
}
if (mAudioManager != null) {
- mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
+ mSilentModeOn = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
}
}
@@ -2367,22 +1942,22 @@ public class LatinIME extends InputMethodService
// if mAudioManager is null, we don't have the ringer state yet
// mAudioManager will be set by updateRingerMode
if (mAudioManager == null) {
- if (mKeyboardSwitcher.getInputView() != null) {
+ if (mKeyboardSwitcher.getKeyboardView() != null) {
updateRingerMode();
}
}
- if (mSoundOn && !mSilentMode) {
+ if (isSoundOn()) {
// FIXME: Volume and enable should come from UI settings
// FIXME: These should be triggered after auto-repeat logic
int sound = AudioManager.FX_KEYPRESS_STANDARD;
switch (primaryCode) {
- case Keyboard.KEYCODE_DELETE:
+ case Keyboard.CODE_DELETE:
sound = AudioManager.FX_KEYPRESS_DELETE;
break;
- case KEYCODE_ENTER:
+ case Keyboard.CODE_ENTER:
sound = AudioManager.FX_KEYPRESS_RETURN;
break;
- case KEYCODE_SPACE:
+ case Keyboard.CODE_SPACE:
sound = AudioManager.FX_KEYPRESS_SPACEBAR;
break;
}
@@ -2390,81 +1965,69 @@ public class LatinIME extends InputMethodService
}
}
- private void vibrate() {
- if (!mVibrateOn) {
+ public void vibrate() {
+ if (!mSettingsValues.mVibrateOn) {
return;
}
- if (mKeyboardSwitcher.getInputView() != null) {
- mKeyboardSwitcher.getInputView().performHapticFeedback(
+ LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
+ if (inputView != null) {
+ inputView.performHapticFeedback(
HapticFeedbackConstants.KEYBOARD_TAP,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
}
}
- private void checkTutorial(String privateImeOptions) {
- if (privateImeOptions == null) return;
- if (privateImeOptions.equals("com.android.setupwizard:ShowTutorial")) {
- if (mTutorial == null) startTutorial();
- } else if (privateImeOptions.equals("com.android.setupwizard:HideTutorial")) {
- if (mTutorial != null) {
- if (mTutorial.close()) {
- mTutorial = null;
- }
- }
- }
- }
-
- private void startTutorial() {
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TUTORIAL), 500);
- }
-
- /* package */ void tutorialDone() {
- mTutorial = null;
- }
-
- /* package */ void promoteToUserDictionary(String word, int frequency) {
- if (mUserDictionary.isValidWord(word)) return;
- mUserDictionary.addWord(word, frequency);
- }
-
- /* package */ WordComposer getCurrentWord() {
+ public WordComposer getCurrentWord() {
return mWord;
}
- /* package */ boolean getPopupOn() {
- return mPopupOn;
+ boolean isSoundOn() {
+ return mSettingsValues.mSoundOn && !mSilentModeOn;
}
private void updateCorrectionMode() {
+ // TODO: cleanup messy flags
mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false;
- mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes)
- && !mInputTypeNoAutoCorrect && mHasDictionary;
- mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled)
+ final boolean shouldAutoCorrect = (mSettingsValues.mAutoCorrectEnabled
+ || mSettingsValues.mQuickFixes) && !mInputTypeNoAutoCorrect && mHasDictionary;
+ mCorrectionMode = (shouldAutoCorrect && mSettingsValues.mAutoCorrectEnabled)
? Suggest.CORRECTION_FULL
- : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
- mCorrectionMode = (mBigramSuggestionEnabled && mAutoCorrectOn && mAutoCorrectEnabled)
+ : (shouldAutoCorrect ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
+ mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect
+ && mSettingsValues.mAutoCorrectEnabled)
? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
if (mSuggest != null) {
mSuggest.setCorrectionMode(mCorrectionMode);
}
}
- private void updateAutoTextEnabled(Locale systemLocale) {
+ private void updateAutoTextEnabled() {
if (mSuggest == null) return;
- boolean different =
- !systemLocale.getLanguage().equalsIgnoreCase(mInputLocale.substring(0, 2));
- mSuggest.setAutoTextEnabled(!different && mQuickFixes);
+ mSuggest.setQuickFixesEnabled(mSettingsValues.mQuickFixes
+ && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage());
+ }
+
+ private void updateSuggestionVisibility(final SharedPreferences prefs, final Resources res) {
+ final String suggestionVisiblityStr = prefs.getString(
+ Settings.PREF_SHOW_SUGGESTIONS_SETTING,
+ res.getString(R.string.prefs_suggestion_visibility_default_value));
+ for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
+ if (suggestionVisiblityStr.equals(res.getString(visibility))) {
+ mSuggestionVisibility = visibility;
+ break;
+ }
+ }
}
protected void launchSettings() {
- launchSettings(LatinIMESettings.class);
+ launchSettings(Settings.class);
}
public void launchDebugSettings() {
- launchSettings(LatinIMEDebugSettings.class);
+ launchSettings(DebugSettings.class);
}
- protected void launchSettings (Class<? extends PreferenceActivity> settingsClass) {
+ protected void launchSettings(Class<? extends PreferenceActivity> settingsClass) {
handleClose();
Intent intent = new Intent();
intent.setClass(LatinIME.this, settingsClass);
@@ -2472,122 +2035,78 @@ public class LatinIME extends InputMethodService
startActivity(intent);
}
- private void loadSettings() {
- // Get the settings preferences
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
- mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, false);
- mSoundOn = sp.getBoolean(PREF_SOUND_ON, false);
- mPopupOn = sp.getBoolean(PREF_POPUP_ON,
- mResources.getBoolean(R.bool.default_popup_preview));
- mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true);
- mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true);
- mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false);
- mHasUsedVoiceInputUnsupportedLocale =
- sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false);
-
- // Get the current list of supported locales and check the current locale against that
- // list. We cache this value so as not to check it every time the user starts a voice
- // input. Because this method is called by onStartInputView, this should mean that as
- // long as the locale doesn't change while the user is keeping the IME open, the
- // value should never be stale.
- String supportedLocalesString = SettingsUtil.getSettingsString(
- getContentResolver(),
- SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES,
- DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES);
- ArrayList<String> voiceInputSupportedLocales =
- newArrayList(supportedLocalesString.split("\\s+"));
-
- mLocaleSupportedForVoiceInput = voiceInputSupportedLocales.contains(mInputLocale);
-
- mShowSuggestions = sp.getBoolean(PREF_SHOW_SUGGESTIONS, true);
-
- if (VOICE_INSTALLED) {
- final String voiceMode = sp.getString(PREF_VOICE_MODE,
- getString(R.string.voice_mode_main));
- boolean enableVoice = !voiceMode.equals(getString(R.string.voice_mode_off))
- && mEnableVoiceButton;
- boolean voiceOnPrimary = voiceMode.equals(getString(R.string.voice_mode_main));
- if (mKeyboardSwitcher != null &&
- (enableVoice != mEnableVoice || voiceOnPrimary != mVoiceOnPrimary)) {
- mKeyboardSwitcher.setVoiceMode(enableVoice, voiceOnPrimary);
+ private void showSubtypeSelectorAndSettings() {
+ final CharSequence title = getString(R.string.english_ime_input_options);
+ final CharSequence[] items = new CharSequence[] {
+ // TODO: Should use new string "Select active input modes".
+ getString(R.string.language_selection_title),
+ getString(R.string.english_ime_settings),
+ };
+ final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface di, int position) {
+ di.dismiss();
+ switch (position) {
+ case 0:
+ Intent intent = CompatUtils.getInputLanguageSelectionIntent(
+ mInputMethodId, Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ break;
+ case 1:
+ launchSettings();
+ break;
+ }
}
- mEnableVoice = enableVoice;
- mVoiceOnPrimary = voiceOnPrimary;
- }
- mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE,
- mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions;
- //mBigramSuggestionEnabled = sp.getBoolean(
- // PREF_BIGRAM_SUGGESTIONS, true) & mShowSuggestions;
- updateCorrectionMode();
- updateAutoTextEnabled(mResources.getConfiguration().locale);
- mLanguageSwitcher.loadLocales(sp);
+ };
+ showOptionsMenuInternal(title, items, listener);
}
- private void initSuggestPuncList() {
- mSuggestPuncList = new ArrayList<CharSequence>();
- mSuggestPuncs = mResources.getString(R.string.suggested_punctuations);
- if (mSuggestPuncs != null) {
- for (int i = 0; i < mSuggestPuncs.length(); i++) {
- mSuggestPuncList.add(mSuggestPuncs.subSequence(i, i + 1));
+ private void showOptionsMenu() {
+ final CharSequence title = getString(R.string.english_ime_input_options);
+ final CharSequence[] items = new CharSequence[] {
+ getString(R.string.selectInputMethod),
+ getString(R.string.english_ime_settings),
+ };
+ final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface di, int position) {
+ di.dismiss();
+ switch (position) {
+ case 0:
+ mImm.showInputMethodPicker();
+ break;
+ case 1:
+ launchSettings();
+ break;
+ }
}
- }
+ };
+ showOptionsMenuInternal(title, items, listener);
}
- private boolean isSuggestedPunctuation(int code) {
- return mSuggestPuncs.contains(String.valueOf((char)code));
- }
-
- private void showOptionsMenu() {
+ private void showOptionsMenuInternal(CharSequence title, CharSequence[] items,
+ DialogInterface.OnClickListener listener) {
+ final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
+ if (windowToken == null) return;
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setCancelable(true);
builder.setIcon(R.drawable.ic_dialog_keyboard);
builder.setNegativeButton(android.R.string.cancel, null);
- CharSequence itemSettings = getString(R.string.english_ime_settings);
- CharSequence itemInputMethod = getString(R.string.selectInputMethod);
- builder.setItems(new CharSequence[] {
- itemInputMethod, itemSettings},
- new DialogInterface.OnClickListener() {
-
- public void onClick(DialogInterface di, int position) {
- di.dismiss();
- switch (position) {
- case POS_SETTINGS:
- launchSettings();
- break;
- case POS_METHOD:
- ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
- .showInputMethodPicker();
- break;
- }
- }
- });
- builder.setTitle(mResources.getString(R.string.english_ime_input_options));
+ builder.setItems(items, listener);
+ builder.setTitle(title);
mOptionsDialog = builder.create();
+ mOptionsDialog.setCanceledOnTouchOutside(true);
Window window = mOptionsDialog.getWindow();
WindowManager.LayoutParams lp = window.getAttributes();
- lp.token = mKeyboardSwitcher.getInputView().getWindowToken();
+ lp.token = windowToken;
lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
window.setAttributes(lp);
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
mOptionsDialog.show();
}
- public void changeKeyboardMode() {
- mKeyboardSwitcher.toggleSymbols();
- if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) {
- mKeyboardSwitcher.setShiftLocked(mCapsLock);
- }
-
- updateShiftKeyState(getCurrentInputEditorInfo());
- }
-
- public static <E> ArrayList<E> newArrayList(E... elements) {
- int capacity = (elements.length * 110) / 100 + 5;
- ArrayList<E> list = new ArrayList<E>(capacity);
- Collections.addAll(list, elements);
- return list;
- }
-
@Override
protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
super.dump(fd, fout, args);
@@ -2595,18 +2114,17 @@ public class LatinIME extends InputMethodService
final Printer p = new PrintWriterPrinter(fout);
p.println("LatinIME state :");
p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode());
- p.println(" mCapsLock=" + mCapsLock);
p.println(" mComposing=" + mComposing.toString());
- p.println(" mPredictionOn=" + mPredictionOn);
+ p.println(" mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn);
p.println(" mCorrectionMode=" + mCorrectionMode);
- p.println(" mPredicting=" + mPredicting);
- p.println(" mAutoCorrectOn=" + mAutoCorrectOn);
- p.println(" mAutoSpace=" + mAutoSpace);
- p.println(" mCompletionOn=" + mCompletionOn);
+ p.println(" mHasUncommittedTypedChars=" + mHasUncommittedTypedChars);
+ p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
+ p.println(" mShouldInsertMagicSpace=" + mShouldInsertMagicSpace);
+ p.println(" mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn);
p.println(" TextEntryState.state=" + TextEntryState.getState());
- p.println(" mSoundOn=" + mSoundOn);
- p.println(" mVibrateOn=" + mVibrateOn);
- p.println(" mPopupOn=" + mPopupOn);
+ p.println(" mSoundOn=" + mSettingsValues.mSoundOn);
+ p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn);
+ p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
}
// Characters per second measurement
@@ -2626,8 +2144,4 @@ public class LatinIME extends InputMethodService
for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i];
System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
}
-
- public void onAutoCompletionStateChanged(boolean isAutoCompletion) {
- mKeyboardSwitcher.onAutoCompletionStateChanged(isAutoCompletion);
- }
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIMESettings.java b/java/src/com/android/inputmethod/latin/LatinIMESettings.java
deleted file mode 100644
index ffff33da2..000000000
--- a/java/src/com/android/inputmethod/latin/LatinIMESettings.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * 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.latin;
-
-import java.util.ArrayList;
-import java.util.Locale;
-
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.backup.BackupManager;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.preference.CheckBoxPreference;
-import android.preference.ListPreference;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceGroup;
-import android.speech.SpeechRecognizer;
-import android.text.AutoText;
-import android.util.Log;
-
-import com.android.inputmethod.voice.SettingsUtil;
-import com.android.inputmethod.voice.VoiceInputLogger;
-
-public class LatinIMESettings extends PreferenceActivity
- implements SharedPreferences.OnSharedPreferenceChangeListener,
- DialogInterface.OnDismissListener {
-
- private static final String QUICK_FIXES_KEY = "quick_fixes";
- private static final String PREDICTION_SETTINGS_KEY = "prediction_settings";
- private static final String VOICE_SETTINGS_KEY = "voice_mode";
- /* package */ static final String PREF_SETTINGS_KEY = "settings_key";
-
- private static final String TAG = "LatinIMESettings";
-
- // Dialog ids
- private static final int VOICE_INPUT_CONFIRM_DIALOG = 0;
-
- private CheckBoxPreference mQuickFixes;
- private ListPreference mVoicePreference;
- private ListPreference mSettingsKeyPreference;
- private boolean mVoiceOn;
-
- private VoiceInputLogger mLogger;
-
- private boolean mOkClicked = false;
- private String mVoiceModeOff;
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- addPreferencesFromResource(R.xml.prefs);
- mQuickFixes = (CheckBoxPreference) findPreference(QUICK_FIXES_KEY);
- mVoicePreference = (ListPreference) findPreference(VOICE_SETTINGS_KEY);
- mSettingsKeyPreference = (ListPreference) findPreference(PREF_SETTINGS_KEY);
- SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
- prefs.registerOnSharedPreferenceChangeListener(this);
-
- mVoiceModeOff = getString(R.string.voice_mode_off);
- mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff));
- mLogger = VoiceInputLogger.getLogger(this);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- int autoTextSize = AutoText.getSize(getListView());
- if (autoTextSize < 1) {
- ((PreferenceGroup) findPreference(PREDICTION_SETTINGS_KEY))
- .removePreference(mQuickFixes);
- }
- if (!LatinIME.VOICE_INSTALLED
- || !SpeechRecognizer.isRecognitionAvailable(this)) {
- getPreferenceScreen().removePreference(mVoicePreference);
- } else {
- updateVoiceModeSummary();
- }
- updateSettingsKeySummary();
- }
-
- @Override
- protected void onDestroy() {
- getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
- this);
- super.onDestroy();
- }
-
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- (new BackupManager(this)).dataChanged();
- // If turning on voice input, show dialog
- if (key.equals(VOICE_SETTINGS_KEY) && !mVoiceOn) {
- if (!prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff)
- .equals(mVoiceModeOff)) {
- showVoiceConfirmation();
- }
- }
- mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff));
- updateVoiceModeSummary();
- updateSettingsKeySummary();
- }
-
- private void updateSettingsKeySummary() {
- mSettingsKeyPreference.setSummary(
- getResources().getStringArray(R.array.settings_key_modes)
- [mSettingsKeyPreference.findIndexOfValue(mSettingsKeyPreference.getValue())]);
- }
-
- private void showVoiceConfirmation() {
- mOkClicked = false;
- showDialog(VOICE_INPUT_CONFIRM_DIALOG);
- }
-
- private void updateVoiceModeSummary() {
- mVoicePreference.setSummary(
- getResources().getStringArray(R.array.voice_input_modes_summary)
- [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
- }
-
- @Override
- protected Dialog onCreateDialog(int id) {
- switch (id) {
- case VOICE_INPUT_CONFIRM_DIALOG:
- DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int whichButton) {
- if (whichButton == DialogInterface.BUTTON_NEGATIVE) {
- mVoicePreference.setValue(mVoiceModeOff);
- mLogger.settingsWarningDialogCancel();
- } else if (whichButton == DialogInterface.BUTTON_POSITIVE) {
- mOkClicked = true;
- mLogger.settingsWarningDialogOk();
- }
- updateVoicePreference();
- }
- };
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.voice_warning_title)
- .setPositiveButton(android.R.string.ok, listener)
- .setNegativeButton(android.R.string.cancel, listener);
-
- // Get the current list of supported locales and check the current locale against
- // that list, to decide whether to put a warning that voice input will not work in
- // the current language as part of the pop-up confirmation dialog.
- String supportedLocalesString = SettingsUtil.getSettingsString(
- getContentResolver(),
- SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES,
- LatinIME.DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES);
- ArrayList<String> voiceInputSupportedLocales =
- LatinIME.newArrayList(supportedLocalesString.split("\\s+"));
- boolean localeSupported = voiceInputSupportedLocales.contains(
- Locale.getDefault().toString());
-
- if (localeSupported) {
- String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" +
- getString(R.string.voice_hint_dialog_message);
- builder.setMessage(message);
- } else {
- String message = getString(R.string.voice_warning_locale_not_supported) +
- "\n\n" + getString(R.string.voice_warning_may_not_understand) + "\n\n" +
- getString(R.string.voice_hint_dialog_message);
- builder.setMessage(message);
- }
-
- AlertDialog dialog = builder.create();
- dialog.setOnDismissListener(this);
- mLogger.settingsWarningDialogShown();
- return dialog;
- default:
- Log.e(TAG, "unknown dialog " + id);
- return null;
- }
- }
-
- public void onDismiss(DialogInterface dialog) {
- mLogger.settingsWarningDialogDismissed();
- if (!mOkClicked) {
- // This assumes that onPreferenceClick gets called first, and this if the user
- // agreed after the warning, we set the mOkClicked value to true.
- mVoicePreference.setValue(mVoiceModeOff);
- }
- }
-
- private void updateVoicePreference() {
- boolean isChecked = !mVoicePreference.getValue().equals(mVoiceModeOff);
- if (isChecked) {
- mLogger.voiceInputSettingEnabled();
- } else {
- mLogger.voiceInputSettingDisabled();
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/LatinIMEUtil.java b/java/src/com/android/inputmethod/latin/LatinIMEUtil.java
deleted file mode 100644
index 85ecaee50..000000000
--- a/java/src/com/android/inputmethod/latin/LatinIMEUtil.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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.latin;
-
-import android.view.inputmethod.InputMethodManager;
-
-import android.content.Context;
-import android.os.AsyncTask;
-import android.text.format.DateUtils;
-import android.util.Log;
-
-public class LatinIMEUtil {
-
- /**
- * Cancel an {@link AsyncTask}.
- *
- * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
- * task should be interrupted; otherwise, in-progress tasks are allowed
- * to complete.
- */
- public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
- if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
- task.cancel(mayInterruptIfRunning);
- }
- }
-
- public static class GCUtils {
- private static final String TAG = "GCUtils";
- public static final int GC_TRY_COUNT = 2;
- // GC_TRY_LOOP_MAX is used for the hard limit of GC wait,
- // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT.
- public static final int GC_TRY_LOOP_MAX = 5;
- private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS;
- private static GCUtils sInstance = new GCUtils();
- private int mGCTryCount = 0;
-
- public static GCUtils getInstance() {
- return sInstance;
- }
-
- public void reset() {
- mGCTryCount = 0;
- }
-
- public boolean tryGCOrWait(String metaData, Throwable t) {
- if (mGCTryCount == 0) {
- System.gc();
- }
- if (++mGCTryCount > GC_TRY_COUNT) {
- LatinImeLogger.logOnException(metaData, t);
- return false;
- } else {
- try {
- Thread.sleep(GC_INTERVAL);
- return true;
- } catch (InterruptedException e) {
- Log.e(TAG, "Sleep was interrupted.");
- LatinImeLogger.logOnException(metaData, t);
- return false;
- }
- }
- }
- }
-
- public static boolean hasMultipleEnabledIMEs(Context context) {
- return ((InputMethodManager) context.getSystemService(
- Context.INPUT_METHOD_SERVICE)).getEnabledInputMethodList().size() > 1;
- }
-
- /* package */ static class RingCharBuffer {
- private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
- private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
- private static final int INVALID_COORDINATE = -2;
- /* package */ static final int BUFSIZE = 20;
- private Context mContext;
- private boolean mEnabled = false;
- private int mEnd = 0;
- /* package */ int mLength = 0;
- private char[] mCharBuf = new char[BUFSIZE];
- private int[] mXBuf = new int[BUFSIZE];
- private int[] mYBuf = new int[BUFSIZE];
-
- private RingCharBuffer() {
- }
- public static RingCharBuffer getInstance() {
- return sRingCharBuffer;
- }
- public static RingCharBuffer init(Context context, boolean enabled) {
- sRingCharBuffer.mContext = context;
- sRingCharBuffer.mEnabled = enabled;
- return sRingCharBuffer;
- }
- private int normalize(int in) {
- int ret = in % BUFSIZE;
- return ret < 0 ? ret + BUFSIZE : ret;
- }
- public void push(char c, int x, int y) {
- if (!mEnabled) return;
- mCharBuf[mEnd] = c;
- mXBuf[mEnd] = x;
- mYBuf[mEnd] = y;
- mEnd = normalize(mEnd + 1);
- if (mLength < BUFSIZE) {
- ++mLength;
- }
- }
- public char pop() {
- if (mLength < 1) {
- return PLACEHOLDER_DELIMITER_CHAR;
- } else {
- mEnd = normalize(mEnd - 1);
- --mLength;
- return mCharBuf[mEnd];
- }
- }
- public char getLastChar() {
- if (mLength < 1) {
- return PLACEHOLDER_DELIMITER_CHAR;
- } else {
- return mCharBuf[normalize(mEnd - 1)];
- }
- }
- public int getPreviousX(char c, int back) {
- int index = normalize(mEnd - 2 - back);
- if (mLength <= back
- || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
- return INVALID_COORDINATE;
- } else {
- return mXBuf[index];
- }
- }
- public int getPreviousY(char c, int back) {
- int index = normalize(mEnd - 2 - back);
- if (mLength <= back
- || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
- return INVALID_COORDINATE;
- } else {
- return mYBuf[index];
- }
- }
- public String getLastString() {
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < mLength; ++i) {
- char c = mCharBuf[normalize(mEnd - 1 - i)];
- if (!((LatinIME)mContext).isWordSeparator(c)) {
- sb.append(c);
- } else {
- break;
- }
- }
- return sb.reverse().toString();
- }
- public void reset() {
- mLength = 0;
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
index a8ab9cc98..e460471a5 100644
--- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java
+++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
@@ -16,19 +16,23 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.latin.Dictionary.DataType;
import android.content.Context;
import android.content.SharedPreferences;
-import android.inputmethodservice.Keyboard;
+
import java.util.List;
public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+ public static boolean sDBG = false;
+
+ @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
}
- public static void init(Context context) {
+ public static void init(Context context, SharedPreferences prefs) {
}
public static void commit() {
@@ -41,10 +45,10 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
String before, String after, int position, List<CharSequence> suggestions) {
}
- public static void logOnAutoSuggestion(String before, String after) {
+ public static void logOnAutoCorrection(String before, String after, int separatorCode) {
}
- public static void logOnAutoSuggestionCanceled() {
+ public static void logOnAutoCorrectionCancelled() {
}
public static void logOnDelete() {
@@ -53,6 +57,9 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
public static void logOnInputChar() {
}
+ public static void logOnInputSeparator() {
+ }
+
public static void logOnException(String metaData, Throwable e) {
}
@@ -68,4 +75,6 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
public static void onSetKeyboard(Keyboard kb) {
}
+ public static void onPrintAllUsabilityStudyLogs() {
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboard.java b/java/src/com/android/inputmethod/latin/LatinKeyboard.java
deleted file mode 100644
index 1438d7da1..000000000
--- a/java/src/com/android/inputmethod/latin/LatinKeyboard.java
+++ /dev/null
@@ -1,1027 +0,0 @@
-/*
- * 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.latin;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.inputmethodservice.Keyboard;
-import android.text.TextPaint;
-import android.util.Log;
-import android.view.ViewConfiguration;
-import android.view.inputmethod.EditorInfo;
-
-import java.util.List;
-import java.util.Locale;
-
-public class LatinKeyboard extends Keyboard {
-
- private static final boolean DEBUG_PREFERRED_LETTER = false;
- private static final String TAG = "LatinKeyboard";
- private static final int OPACITY_FULLY_OPAQUE = 255;
- private static final int SPACE_LED_LENGTH_PERCENT = 80;
-
- private Drawable mShiftLockIcon;
- private Drawable mShiftLockPreviewIcon;
- private Drawable mOldShiftIcon;
- private Drawable mSpaceIcon;
- private Drawable mSpaceAutoCompletionIndicator;
- private Drawable mSpacePreviewIcon;
- private Drawable mMicIcon;
- private Drawable mMicPreviewIcon;
- private Drawable m123MicIcon;
- private Drawable m123MicPreviewIcon;
- private final Drawable mButtonArrowLeftIcon;
- private final Drawable mButtonArrowRightIcon;
- private Key mShiftKey;
- private Key mEnterKey;
- private Key mF1Key;
- private final Drawable mHintIcon;
- private Key mSpaceKey;
- private Key m123Key;
- private final int NUMBER_HINT_COUNT = 10;
- private Key[] mNumberHintKeys;
- private Drawable[] mNumberHintIcons = new Drawable[NUMBER_HINT_COUNT];
- private final int[] mSpaceKeyIndexArray;
- private int mSpaceDragStartX;
- private int mSpaceDragLastDiff;
- private Locale mLocale;
- private LanguageSwitcher mLanguageSwitcher;
- private final Resources mRes;
- private final Context mContext;
- private int mMode;
- // Whether this keyboard has voice icon on it
- private boolean mHasVoiceButton;
- // Whether voice icon is enabled at all
- private boolean mVoiceEnabled;
- private final boolean mIsAlphaKeyboard;
- private CharSequence m123Label;
- private boolean mCurrentlyInSpace;
- private SlidingLocaleDrawable mSlidingLocaleIcon;
- private int[] mPrefLetterFrequencies;
- private int mPrefLetter;
- private int mPrefLetterX;
- private int mPrefLetterY;
- private int mPrefDistance;
-
- // TODO: generalize for any keyboardId
- private boolean mIsBlackSym;
-
- // TODO: remove this attribute when either Keyboard.mDefaultVerticalGap or Key.parent becomes
- // non-private.
- private final int mVerticalGap;
-
- private static final int SHIFT_OFF = 0;
- private static final int SHIFT_ON = 1;
- private static final int SHIFT_LOCKED = 2;
-
- private int mShiftState = SHIFT_OFF;
-
- private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f;
- private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f;
- private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f;
- // Minimum width of space key preview (proportional to keyboard width)
- private static final float SPACEBAR_POPUP_MIN_RATIO = 0.4f;
- // Height in space key the language name will be drawn. (proportional to space key height)
- private static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f;
- // If the full language name needs to be smaller than this value to be drawn on space key,
- // its short language name will be used instead.
- private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f;
-
- private static int sSpacebarVerticalCorrection;
-
- public LatinKeyboard(Context context, int xmlLayoutResId) {
- this(context, xmlLayoutResId, 0);
- }
-
- public LatinKeyboard(Context context, int xmlLayoutResId, int mode) {
- super(context, xmlLayoutResId, mode);
- final Resources res = context.getResources();
- mContext = context;
- mMode = mode;
- mRes = res;
- mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked);
- mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked);
- setDefaultBounds(mShiftLockPreviewIcon);
- mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space);
- mSpaceAutoCompletionIndicator = res.getDrawable(R.drawable.sym_keyboard_space_led);
- mSpacePreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_space);
- mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic);
- mMicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_mic);
- setDefaultBounds(mMicPreviewIcon);
- mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left);
- mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right);
- m123MicIcon = res.getDrawable(R.drawable.sym_keyboard_123_mic);
- m123MicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_123_mic);
- mHintIcon = res.getDrawable(R.drawable.hint_popup);
- setDefaultBounds(m123MicPreviewIcon);
- sSpacebarVerticalCorrection = res.getDimensionPixelOffset(
- R.dimen.spacebar_vertical_correction);
- mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty
- || xmlLayoutResId == R.xml.kbd_qwerty_black;
- // The index of space key is available only after Keyboard constructor has finished.
- mSpaceKeyIndexArray = new int[] { indexOf(LatinIME.KEYCODE_SPACE) };
- initializeNumberHintResources(context);
- // TODO remove this initialization after cleanup
- mVerticalGap = super.getVerticalGap();
- }
-
- private void initializeNumberHintResources(Context context) {
- final Resources res = context.getResources();
- mNumberHintIcons[0] = res.getDrawable(R.drawable.keyboard_hint_0);
- mNumberHintIcons[1] = res.getDrawable(R.drawable.keyboard_hint_1);
- mNumberHintIcons[2] = res.getDrawable(R.drawable.keyboard_hint_2);
- mNumberHintIcons[3] = res.getDrawable(R.drawable.keyboard_hint_3);
- mNumberHintIcons[4] = res.getDrawable(R.drawable.keyboard_hint_4);
- mNumberHintIcons[5] = res.getDrawable(R.drawable.keyboard_hint_5);
- mNumberHintIcons[6] = res.getDrawable(R.drawable.keyboard_hint_6);
- mNumberHintIcons[7] = res.getDrawable(R.drawable.keyboard_hint_7);
- mNumberHintIcons[8] = res.getDrawable(R.drawable.keyboard_hint_8);
- mNumberHintIcons[9] = res.getDrawable(R.drawable.keyboard_hint_9);
- }
-
- @Override
- protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
- XmlResourceParser parser) {
- Key key = new LatinKey(res, parent, x, y, parser);
- switch (key.codes[0]) {
- case LatinIME.KEYCODE_ENTER:
- mEnterKey = key;
- break;
- case LatinKeyboardView.KEYCODE_F1:
- mF1Key = key;
- break;
- case LatinIME.KEYCODE_SPACE:
- mSpaceKey = key;
- break;
- case KEYCODE_MODE_CHANGE:
- m123Key = key;
- m123Label = key.label;
- break;
- }
-
- // For number hints on the upper-right corner of key
- if (mNumberHintKeys == null) {
- // NOTE: This protected method is being called from the base class constructor before
- // mNumberHintKeys gets initialized.
- mNumberHintKeys = new Key[NUMBER_HINT_COUNT];
- }
- int hintNumber = -1;
- if (LatinKeyboardBaseView.isNumberAtLeftmostPopupChar(key)) {
- hintNumber = key.popupCharacters.charAt(0) - '0';
- } else if (LatinKeyboardBaseView.isNumberAtRightmostPopupChar(key)) {
- hintNumber = key.popupCharacters.charAt(key.popupCharacters.length() - 1) - '0';
- }
- if (hintNumber >= 0 && hintNumber <= 9) {
- mNumberHintKeys[hintNumber] = key;
- }
-
- return key;
- }
-
- void setImeOptions(Resources res, int mode, int options) {
- mMode = mode;
- // TODO should clean up this method
- if (mEnterKey != null) {
- // Reset some of the rarely used attributes.
- mEnterKey.popupCharacters = null;
- mEnterKey.popupResId = 0;
- mEnterKey.text = null;
- switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) {
- case EditorInfo.IME_ACTION_GO:
- mEnterKey.iconPreview = null;
- mEnterKey.icon = null;
- mEnterKey.label = res.getText(R.string.label_go_key);
- break;
- case EditorInfo.IME_ACTION_NEXT:
- mEnterKey.iconPreview = null;
- mEnterKey.icon = null;
- mEnterKey.label = res.getText(R.string.label_next_key);
- break;
- case EditorInfo.IME_ACTION_DONE:
- mEnterKey.iconPreview = null;
- mEnterKey.icon = null;
- mEnterKey.label = res.getText(R.string.label_done_key);
- break;
- case EditorInfo.IME_ACTION_SEARCH:
- mEnterKey.iconPreview = res.getDrawable(
- R.drawable.sym_keyboard_feedback_search);
- mEnterKey.icon = res.getDrawable(mIsBlackSym ?
- R.drawable.sym_bkeyboard_search : R.drawable.sym_keyboard_search);
- mEnterKey.label = null;
- break;
- case EditorInfo.IME_ACTION_SEND:
- mEnterKey.iconPreview = null;
- mEnterKey.icon = null;
- mEnterKey.label = res.getText(R.string.label_send_key);
- break;
- default:
- if (mode == KeyboardSwitcher.MODE_IM) {
- mEnterKey.icon = mHintIcon;
- mEnterKey.iconPreview = null;
- mEnterKey.label = ":-)";
- mEnterKey.text = ":-) ";
- mEnterKey.popupResId = R.xml.popup_smileys;
- } else {
- mEnterKey.iconPreview = res.getDrawable(
- R.drawable.sym_keyboard_feedback_return);
- mEnterKey.icon = res.getDrawable(mIsBlackSym ?
- R.drawable.sym_bkeyboard_return : R.drawable.sym_keyboard_return);
- mEnterKey.label = null;
- }
- break;
- }
- // Set the initial size of the preview icon
- if (mEnterKey.iconPreview != null) {
- setDefaultBounds(mEnterKey.iconPreview);
- }
- }
- }
-
- void enableShiftLock() {
- int index = getShiftKeyIndex();
- if (index >= 0) {
- mShiftKey = getKeys().get(index);
- if (mShiftKey instanceof LatinKey) {
- ((LatinKey)mShiftKey).enableShiftLock();
- }
- mOldShiftIcon = mShiftKey.icon;
- }
- }
-
- void setShiftLocked(boolean shiftLocked) {
- if (mShiftKey != null) {
- if (shiftLocked) {
- mShiftKey.on = true;
- mShiftKey.icon = mShiftLockIcon;
- mShiftState = SHIFT_LOCKED;
- } else {
- mShiftKey.on = false;
- mShiftKey.icon = mShiftLockIcon;
- mShiftState = SHIFT_ON;
- }
- }
- }
-
- boolean isShiftLocked() {
- return mShiftState == SHIFT_LOCKED;
- }
-
- @Override
- public boolean setShifted(boolean shiftState) {
- boolean shiftChanged = false;
- if (mShiftKey != null) {
- if (shiftState == false) {
- shiftChanged = mShiftState != SHIFT_OFF;
- mShiftState = SHIFT_OFF;
- mShiftKey.on = false;
- mShiftKey.icon = mOldShiftIcon;
- } else {
- if (mShiftState == SHIFT_OFF) {
- shiftChanged = mShiftState == SHIFT_OFF;
- mShiftState = SHIFT_ON;
- mShiftKey.icon = mShiftLockIcon;
- }
- }
- } else {
- return super.setShifted(shiftState);
- }
- return shiftChanged;
- }
-
- @Override
- public boolean isShifted() {
- if (mShiftKey != null) {
- return mShiftState != SHIFT_OFF;
- } else {
- return super.isShifted();
- }
- }
-
- /* package */ boolean isAlphaKeyboard() {
- return mIsAlphaKeyboard;
- }
-
- public void setColorOfSymbolIcons(boolean isAutoCompletion, boolean isBlack) {
- mIsBlackSym = isBlack;
- if (isBlack) {
- mShiftLockIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_shift_locked);
- mSpaceIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_space);
- mMicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_mic);
- m123MicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_123_mic);
- } else {
- mShiftLockIcon = mRes.getDrawable(R.drawable.sym_keyboard_shift_locked);
- mSpaceIcon = mRes.getDrawable(R.drawable.sym_keyboard_space);
- mMicIcon = mRes.getDrawable(R.drawable.sym_keyboard_mic);
- m123MicIcon = mRes.getDrawable(R.drawable.sym_keyboard_123_mic);
- }
- updateDynamicKeys();
- if (mSpaceKey != null) {
- updateSpaceBarForLocale(isAutoCompletion, isBlack);
- }
- updateNumberHintKeys();
- }
-
- private void setDefaultBounds(Drawable drawable) {
- drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
- }
-
- public void setVoiceMode(boolean hasVoiceButton, boolean hasVoice) {
- mHasVoiceButton = hasVoiceButton;
- mVoiceEnabled = hasVoice;
- updateDynamicKeys();
- }
-
- private void updateDynamicKeys() {
- update123Key();
- updateF1Key();
- }
-
- private void update123Key() {
- // Update KEYCODE_MODE_CHANGE key only on alphabet mode, not on symbol mode.
- if (m123Key != null && mIsAlphaKeyboard) {
- if (mVoiceEnabled && !mHasVoiceButton) {
- m123Key.icon = m123MicIcon;
- m123Key.iconPreview = m123MicPreviewIcon;
- m123Key.label = null;
- } else {
- m123Key.icon = null;
- m123Key.iconPreview = null;
- m123Key.label = m123Label;
- }
- }
- }
-
- private void updateF1Key() {
- // Update KEYCODE_F1 key. Please note that some keyboard layouts have no F1 key.
- if (mF1Key == null)
- return;
-
- if (mIsAlphaKeyboard) {
- if (mMode == KeyboardSwitcher.MODE_URL) {
- setNonMicF1Key(mF1Key, "/", R.xml.popup_slash);
- } else if (mMode == KeyboardSwitcher.MODE_EMAIL) {
- setNonMicF1Key(mF1Key, "@", R.xml.popup_at);
- } else {
- if (mVoiceEnabled && mHasVoiceButton) {
- setMicF1Key(mF1Key);
- } else {
- setNonMicF1Key(mF1Key, ",", R.xml.popup_comma);
- }
- }
- } else { // Symbols keyboard
- if (mVoiceEnabled && mHasVoiceButton) {
- setMicF1Key(mF1Key);
- } else {
- setNonMicF1Key(mF1Key, ",", R.xml.popup_comma);
- }
- }
- }
-
- private void setMicF1Key(Key key) {
- // HACK: draw mMicIcon and mHintIcon at the same time
- final Drawable micWithSettingsHintDrawable = new BitmapDrawable(mRes,
- drawSynthesizedSettingsHintImage(key.width, key.height, mMicIcon, mHintIcon));
-
- key.label = null;
- key.codes = new int[] { LatinKeyboardView.KEYCODE_VOICE };
- key.popupResId = R.xml.popup_mic;
- key.icon = micWithSettingsHintDrawable;
- key.iconPreview = mMicPreviewIcon;
- }
-
- private void setNonMicF1Key(Key key, String label, int popupResId) {
- key.label = label;
- key.codes = new int[] { label.charAt(0) };
- key.popupResId = popupResId;
- key.icon = mHintIcon;
- key.iconPreview = null;
- }
-
- public boolean isF1Key(Key key) {
- return key == mF1Key;
- }
-
- public static boolean hasPuncOrSmileysPopup(Key key) {
- return key.popupResId == R.xml.popup_punctuation || key.popupResId == R.xml.popup_smileys;
- }
-
- /**
- * @return a key which should be invalidated.
- */
- public Key onAutoCompletionStateChanged(boolean isAutoCompletion) {
- updateSpaceBarForLocale(isAutoCompletion, mIsBlackSym);
- return mSpaceKey;
- }
-
- private void updateNumberHintKeys() {
- for (int i = 0; i < mNumberHintKeys.length; ++i) {
- if (mNumberHintKeys[i] != null) {
- mNumberHintKeys[i].icon = mNumberHintIcons[i];
- }
- }
- }
-
- public boolean isLanguageSwitchEnabled() {
- return mLocale != null;
- }
-
- private void updateSpaceBarForLocale(boolean isAutoCompletion, boolean isBlack) {
- // If application locales are explicitly selected.
- if (mLocale != null) {
- mSpaceKey.icon = new BitmapDrawable(mRes,
- drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack));
- } else {
- // sym_keyboard_space_led can be shared with Black and White symbol themes.
- if (isAutoCompletion) {
- mSpaceKey.icon = new BitmapDrawable(mRes,
- drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack));
- } else {
- mSpaceKey.icon = isBlack ? mRes.getDrawable(R.drawable.sym_bkeyboard_space)
- : mRes.getDrawable(R.drawable.sym_keyboard_space);
- }
- }
- }
-
- // Compute width of text with specified text size using paint.
- private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) {
- paint.setTextSize(textSize);
- paint.getTextBounds(text, 0, text.length(), bounds);
- return bounds.width();
- }
-
- // Overlay two images: mainIcon and hintIcon.
- private Bitmap drawSynthesizedSettingsHintImage(
- int width, int height, Drawable mainIcon, Drawable hintIcon) {
- if (mainIcon == null || hintIcon == null)
- return null;
- Rect hintIconPadding = new Rect(0, 0, 0, 0);
- hintIcon.getPadding(hintIconPadding);
- final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(buffer);
- canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR);
-
- // Draw main icon at the center of the key visual
- // Assuming the hintIcon shares the same padding with the key's background drawable
- final int drawableX = (width + hintIconPadding.left - hintIconPadding.right
- - mainIcon.getIntrinsicWidth()) / 2;
- final int drawableY = (height + hintIconPadding.top - hintIconPadding.bottom
- - mainIcon.getIntrinsicHeight()) / 2;
- setDefaultBounds(mainIcon);
- canvas.translate(drawableX, drawableY);
- mainIcon.draw(canvas);
- canvas.translate(-drawableX, -drawableY);
-
- // Draw hint icon fully in the key
- hintIcon.setBounds(0, 0, width, height);
- hintIcon.draw(canvas);
- return buffer;
- }
-
- // Layout local language name and left and right arrow on space bar.
- private static String layoutSpaceBar(Paint paint, Locale locale, Drawable lArrow,
- Drawable rArrow, int width, int height, float origTextSize,
- boolean allowVariableTextSize) {
- final float arrowWidth = lArrow.getIntrinsicWidth();
- final float arrowHeight = lArrow.getIntrinsicHeight();
- final float maxTextWidth = width - (arrowWidth + arrowWidth);
- final Rect bounds = new Rect();
-
- // Estimate appropriate language name text size to fit in maxTextWidth.
- String language = LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale), locale);
- int textWidth = getTextWidth(paint, language, origTextSize, bounds);
- // Assuming text width and text size are proportional to each other.
- float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
-
- final boolean useShortName;
- if (allowVariableTextSize) {
- textWidth = getTextWidth(paint, language, textSize, bounds);
- // If text size goes too small or text does not fit, use short name
- useShortName = textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME
- || textWidth > maxTextWidth;
- } else {
- useShortName = textWidth > maxTextWidth;
- textSize = origTextSize;
- }
- if (useShortName) {
- language = LanguageSwitcher.toTitleCase(locale.getLanguage(), locale);
- textWidth = getTextWidth(paint, language, origTextSize, bounds);
- textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
- }
- paint.setTextSize(textSize);
-
- // Place left and right arrow just before and after language text.
- final float baseline = height * SPACEBAR_LANGUAGE_BASELINE;
- final int top = (int)(baseline - arrowHeight);
- final float remains = (width - textWidth) / 2;
- lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline);
- rArrow.setBounds((int)(remains + textWidth), top, (int)(remains + textWidth + arrowWidth),
- (int)baseline);
-
- return language;
- }
-
- private Bitmap drawSpaceBar(int opacity, boolean isAutoCompletion, boolean isBlack) {
- final int width = mSpaceKey.width;
- final int height = mSpaceIcon.getIntrinsicHeight();
- final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(buffer);
- canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR);
-
- // If application locales are explicitly selected.
- if (mLocale != null) {
- final Paint paint = new Paint();
- paint.setAlpha(opacity);
- paint.setAntiAlias(true);
- paint.setTextAlign(Align.CENTER);
-
- final boolean allowVariableTextSize = true;
- final String language = layoutSpaceBar(paint, mLanguageSwitcher.getInputLocale(),
- mButtonArrowLeftIcon, mButtonArrowRightIcon, width, height,
- getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14),
- allowVariableTextSize);
-
- // Draw language text with shadow
- final int shadowColor = mRes.getColor(isBlack
- ? R.color.latinkeyboard_bar_language_shadow_black
- : R.color.latinkeyboard_bar_language_shadow_white);
- final float baseline = height * SPACEBAR_LANGUAGE_BASELINE;
- final float descent = paint.descent();
- paint.setColor(shadowColor);
- canvas.drawText(language, width / 2, baseline - descent - 1, paint);
- paint.setColor(mRes.getColor(R.color.latinkeyboard_bar_language_text));
- canvas.drawText(language, width / 2, baseline - descent, paint);
-
- // Put arrows that are already layed out on either side of the text
- if (mLanguageSwitcher.getLocaleCount() > 1) {
- mButtonArrowLeftIcon.draw(canvas);
- mButtonArrowRightIcon.draw(canvas);
- }
- }
-
- // Draw the spacebar icon at the bottom
- if (isAutoCompletion) {
- final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
- final int iconHeight = mSpaceAutoCompletionIndicator.getIntrinsicHeight();
- int x = (width - iconWidth) / 2;
- int y = height - iconHeight;
- mSpaceAutoCompletionIndicator.setBounds(x, y, x + iconWidth, y + iconHeight);
- mSpaceAutoCompletionIndicator.draw(canvas);
- } else {
- final int iconWidth = mSpaceIcon.getIntrinsicWidth();
- final int iconHeight = mSpaceIcon.getIntrinsicHeight();
- int x = (width - iconWidth) / 2;
- int y = height - iconHeight;
- mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
- mSpaceIcon.draw(canvas);
- }
- return buffer;
- }
-
- private void updateLocaleDrag(int diff) {
- if (mSlidingLocaleIcon == null) {
- final int width = Math.max(mSpaceKey.width,
- (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO));
- final int height = mSpacePreviewIcon.getIntrinsicHeight();
- mSlidingLocaleIcon = new SlidingLocaleDrawable(mSpacePreviewIcon, width, height);
- mSlidingLocaleIcon.setBounds(0, 0, width, height);
- mSpaceKey.iconPreview = mSlidingLocaleIcon;
- }
- mSlidingLocaleIcon.setDiff(diff);
- if (Math.abs(diff) == Integer.MAX_VALUE) {
- mSpaceKey.iconPreview = mSpacePreviewIcon;
- } else {
- mSpaceKey.iconPreview = mSlidingLocaleIcon;
- }
- mSpaceKey.iconPreview.invalidateSelf();
- }
-
- public int getLanguageChangeDirection() {
- if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2
- || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD ) {
- return 0; // No change
- }
- return mSpaceDragLastDiff > 0 ? 1 : -1;
- }
-
- public void setLanguageSwitcher(LanguageSwitcher switcher, boolean isAutoCompletion,
- boolean isBlackSym) {
- mLanguageSwitcher = switcher;
- Locale locale = mLanguageSwitcher.getLocaleCount() > 0
- ? mLanguageSwitcher.getInputLocale()
- : null;
- // If the language count is 1 and is the same as the system language, don't show it.
- if (locale != null
- && mLanguageSwitcher.getLocaleCount() == 1
- && mLanguageSwitcher.getSystemLocale().getLanguage()
- .equalsIgnoreCase(locale.getLanguage())) {
- locale = null;
- }
- mLocale = locale;
- setColorOfSymbolIcons(isAutoCompletion, isBlackSym);
- }
-
- public Locale getInputLocale() {
- return (mLocale != null) ? mLocale : mLanguageSwitcher.getSystemLocale();
- }
-
- boolean isCurrentlyInSpace() {
- return mCurrentlyInSpace;
- }
-
- void setPreferredLetters(int[] frequencies) {
- mPrefLetterFrequencies = frequencies;
- mPrefLetter = 0;
- }
-
- void keyReleased() {
- mCurrentlyInSpace = false;
- mSpaceDragLastDiff = 0;
- mPrefLetter = 0;
- mPrefLetterX = 0;
- mPrefLetterY = 0;
- mPrefDistance = Integer.MAX_VALUE;
- if (mSpaceKey != null) {
- updateLocaleDrag(Integer.MAX_VALUE);
- }
- }
-
- /**
- * Does the magic of locking the touch gesture into the spacebar when
- * switching input languages.
- */
- boolean isInside(LatinKey key, int x, int y) {
- final int code = key.codes[0];
- if (code == KEYCODE_SHIFT ||
- code == KEYCODE_DELETE) {
- y -= key.height / 10;
- if (code == KEYCODE_SHIFT) x += key.width / 6;
- if (code == KEYCODE_DELETE) x -= key.width / 6;
- } else if (code == LatinIME.KEYCODE_SPACE) {
- y += LatinKeyboard.sSpacebarVerticalCorrection;
- if (mLanguageSwitcher.getLocaleCount() > 1) {
- if (mCurrentlyInSpace) {
- int diff = x - mSpaceDragStartX;
- if (Math.abs(diff - mSpaceDragLastDiff) > 0) {
- updateLocaleDrag(diff);
- }
- mSpaceDragLastDiff = diff;
- return true;
- } else {
- boolean insideSpace = key.isInsideSuper(x, y);
- if (insideSpace) {
- mCurrentlyInSpace = true;
- mSpaceDragStartX = x;
- updateLocaleDrag(0);
- }
- return insideSpace;
- }
- }
- } else if (mPrefLetterFrequencies != null) {
- // New coordinate? Reset
- if (mPrefLetterX != x || mPrefLetterY != y) {
- mPrefLetter = 0;
- mPrefDistance = Integer.MAX_VALUE;
- }
- // Handle preferred next letter
- final int[] pref = mPrefLetterFrequencies;
- if (mPrefLetter > 0) {
- if (DEBUG_PREFERRED_LETTER) {
- if (mPrefLetter == code && !key.isInsideSuper(x, y)) {
- Log.d(TAG, "CORRECTED !!!!!!");
- }
- }
- return mPrefLetter == code;
- } else {
- final boolean inside = key.isInsideSuper(x, y);
- int[] nearby = getNearestKeys(x, y);
- List<Key> nearbyKeys = getKeys();
- if (inside) {
- // If it's a preferred letter
- if (inPrefList(code, pref)) {
- // Check if its frequency is much lower than a nearby key
- mPrefLetter = code;
- mPrefLetterX = x;
- mPrefLetterY = y;
- for (int i = 0; i < nearby.length; i++) {
- Key k = nearbyKeys.get(nearby[i]);
- if (k != key && inPrefList(k.codes[0], pref)) {
- final int dist = distanceFrom(k, x, y);
- if (dist < (int) (k.width * OVERLAP_PERCENTAGE_LOW_PROB) &&
- (pref[k.codes[0]] > pref[mPrefLetter] * 3)) {
- mPrefLetter = k.codes[0];
- mPrefDistance = dist;
- if (DEBUG_PREFERRED_LETTER) {
- Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!");
- }
- break;
- }
- }
- }
-
- return mPrefLetter == code;
- }
- }
-
- // Get the surrounding keys and intersect with the preferred list
- // For all in the intersection
- // if distance from touch point is within a reasonable distance
- // make this the pref letter
- // If no pref letter
- // return inside;
- // else return thiskey == prefletter;
-
- for (int i = 0; i < nearby.length; i++) {
- Key k = nearbyKeys.get(nearby[i]);
- if (inPrefList(k.codes[0], pref)) {
- final int dist = distanceFrom(k, x, y);
- if (dist < (int) (k.width * OVERLAP_PERCENTAGE_HIGH_PROB)
- && dist < mPrefDistance) {
- mPrefLetter = k.codes[0];
- mPrefLetterX = x;
- mPrefLetterY = y;
- mPrefDistance = dist;
- }
- }
- }
- // Didn't find any
- if (mPrefLetter == 0) {
- return inside;
- } else {
- return mPrefLetter == code;
- }
- }
- }
-
- // Lock into the spacebar
- if (mCurrentlyInSpace) return false;
-
- return key.isInsideSuper(x, y);
- }
-
- private boolean inPrefList(int code, int[] pref) {
- if (code < pref.length && code >= 0) return pref[code] > 0;
- return false;
- }
-
- private int distanceFrom(Key k, int x, int y) {
- if (y > k.y && y < k.y + k.height) {
- return Math.abs(k.x + k.width / 2 - x);
- } else {
- return Integer.MAX_VALUE;
- }
- }
-
- @Override
- public int[] getNearestKeys(int x, int y) {
- if (mCurrentlyInSpace) {
- return mSpaceKeyIndexArray;
- } else {
- // Avoid dead pixels at edges of the keyboard
- return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)),
- Math.max(0, Math.min(y, getHeight() - 1)));
- }
- }
-
- private int indexOf(int code) {
- List<Key> keys = getKeys();
- int count = keys.size();
- for (int i = 0; i < count; i++) {
- if (keys.get(i).codes[0] == code) return i;
- }
- return -1;
- }
-
- private int getTextSizeFromTheme(int style, int defValue) {
- TypedArray array = mContext.getTheme().obtainStyledAttributes(
- style, new int[] { android.R.attr.textSize });
- int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue);
- return textSize;
- }
-
- // TODO LatinKey could be static class
- class LatinKey extends Keyboard.Key {
-
- // functional normal state (with properties)
- private final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
- android.R.attr.state_single
- };
-
- // functional pressed state (with properties)
- private final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
- android.R.attr.state_single,
- android.R.attr.state_pressed
- };
-
- private boolean mShiftLockEnabled;
-
- public LatinKey(Resources res, Keyboard.Row parent, int x, int y,
- XmlResourceParser parser) {
- super(res, parent, x, y, parser);
- if (popupCharacters != null && popupCharacters.length() == 0) {
- // If there is a keyboard with no keys specified in popupCharacters
- popupResId = 0;
- }
- }
-
- private void enableShiftLock() {
- mShiftLockEnabled = true;
- }
-
- // sticky is used for shift key. If a key is not sticky and is modifier,
- // the key will be treated as functional.
- private boolean isFunctionalKey() {
- return !sticky && modifier;
- }
-
- @Override
- public void onReleased(boolean inside) {
- if (!mShiftLockEnabled) {
- super.onReleased(inside);
- } else {
- pressed = !pressed;
- }
- }
-
- /**
- * Overriding this method so that we can reduce the target area for certain keys.
- */
- @Override
- public boolean isInside(int x, int y) {
- // TODO This should be done by parent.isInside(this, x, y)
- // if Key.parent were protected.
- boolean result = LatinKeyboard.this.isInside(this, x, y);
- return result;
- }
-
- boolean isInsideSuper(int x, int y) {
- return super.isInside(x, y);
- }
-
- @Override
- public int[] getCurrentDrawableState() {
- if (isFunctionalKey()) {
- if (pressed) {
- return KEY_STATE_FUNCTIONAL_PRESSED;
- } else {
- return KEY_STATE_FUNCTIONAL_NORMAL;
- }
- }
- return super.getCurrentDrawableState();
- }
-
- @Override
- public int squaredDistanceFrom(int x, int y) {
- // We should count vertical gap between rows to calculate the center of this Key.
- final int verticalGap = LatinKeyboard.this.mVerticalGap;
- final int xDist = this.x + width / 2 - x;
- final int yDist = this.y + (height + verticalGap) / 2 - y;
- return xDist * xDist + yDist * yDist;
- }
- }
-
- /**
- * Animation to be displayed on the spacebar preview popup when switching
- * languages by swiping the spacebar. It draws the current, previous and
- * next languages and moves them by the delta of touch movement on the spacebar.
- */
- class SlidingLocaleDrawable extends Drawable {
-
- private final int mWidth;
- private final int mHeight;
- private final Drawable mBackground;
- private final TextPaint mTextPaint;
- private final int mMiddleX;
- private final Drawable mLeftDrawable;
- private final Drawable mRightDrawable;
- private final int mThreshold;
- private int mDiff;
- private boolean mHitThreshold;
- private String mCurrentLanguage;
- private String mNextLanguage;
- private String mPrevLanguage;
-
- public SlidingLocaleDrawable(Drawable background, int width, int height) {
- mBackground = background;
- setDefaultBounds(mBackground);
- mWidth = width;
- mHeight = height;
- mTextPaint = new TextPaint();
- mTextPaint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18));
- mTextPaint.setColor(R.color.latinkeyboard_transparent);
- mTextPaint.setTextAlign(Align.CENTER);
- mTextPaint.setAlpha(OPACITY_FULLY_OPAQUE);
- mTextPaint.setAntiAlias(true);
- mMiddleX = (mWidth - mBackground.getIntrinsicWidth()) / 2;
- mLeftDrawable =
- mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_left);
- mRightDrawable =
- mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_right);
- mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop();
- }
-
- private void setDiff(int diff) {
- if (diff == Integer.MAX_VALUE) {
- mHitThreshold = false;
- mCurrentLanguage = null;
- return;
- }
- mDiff = diff;
- if (mDiff > mWidth) mDiff = mWidth;
- if (mDiff < -mWidth) mDiff = -mWidth;
- if (Math.abs(mDiff) > mThreshold) mHitThreshold = true;
- invalidateSelf();
- }
-
- private String getLanguageName(Locale locale) {
- return LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale), locale);
- }
-
- @Override
- public void draw(Canvas canvas) {
- canvas.save();
- if (mHitThreshold) {
- Paint paint = mTextPaint;
- final int width = mWidth;
- final int height = mHeight;
- final int diff = mDiff;
- final Drawable lArrow = mLeftDrawable;
- final Drawable rArrow = mRightDrawable;
- canvas.clipRect(0, 0, width, height);
- if (mCurrentLanguage == null) {
- final LanguageSwitcher languageSwitcher = mLanguageSwitcher;
- mCurrentLanguage = getLanguageName(languageSwitcher.getInputLocale());
- mNextLanguage = getLanguageName(languageSwitcher.getNextInputLocale());
- mPrevLanguage = getLanguageName(languageSwitcher.getPrevInputLocale());
- }
- // Draw language text with shadow
- final float baseline = mHeight * SPACEBAR_LANGUAGE_BASELINE - paint.descent();
- paint.setColor(mRes.getColor(R.color.latinkeyboard_feedback_language_text));
- canvas.drawText(mCurrentLanguage, width / 2 + diff, baseline, paint);
- canvas.drawText(mNextLanguage, diff - width / 2, baseline, paint);
- canvas.drawText(mPrevLanguage, diff + width + width / 2, baseline, paint);
-
- setDefaultBounds(lArrow);
- rArrow.setBounds(width - rArrow.getIntrinsicWidth(), 0, width,
- rArrow.getIntrinsicHeight());
- lArrow.draw(canvas);
- rArrow.draw(canvas);
- }
- if (mBackground != null) {
- canvas.translate(mMiddleX, 0);
- mBackground.draw(canvas);
- }
- canvas.restore();
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- @Override
- public void setAlpha(int alpha) {
- // Ignore
- }
-
- @Override
- public void setColorFilter(ColorFilter cf) {
- // Ignore
- }
-
- @Override
- public int getIntrinsicWidth() {
- return mWidth;
- }
-
- @Override
- public int getIntrinsicHeight() {
- return mHeight;
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
deleted file mode 100644
index fece78689..000000000
--- a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
+++ /dev/null
@@ -1,1517 +0,0 @@
-/*
- * 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.latin;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.Region.Op;
-import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
-import android.inputmethodservice.Keyboard;
-import android.inputmethodservice.Keyboard.Key;
-import android.os.Handler;
-import android.os.Message;
-import android.os.SystemClock;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.GestureDetector;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.PopupWindow;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-import java.util.WeakHashMap;
-
-/**
- * A view that renders a virtual {@link LatinKeyboard}. It handles rendering of keys and
- * detecting key presses and touch movements.
- *
- * TODO: References to LatinKeyboard in this class should be replaced with ones to its base class.
- *
- * @attr ref R.styleable#LatinKeyboardBaseView_keyBackground
- * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewLayout
- * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewOffset
- * @attr ref R.styleable#LatinKeyboardBaseView_labelTextSize
- * @attr ref R.styleable#LatinKeyboardBaseView_keyTextSize
- * @attr ref R.styleable#LatinKeyboardBaseView_keyTextColor
- * @attr ref R.styleable#LatinKeyboardBaseView_verticalCorrection
- * @attr ref R.styleable#LatinKeyboardBaseView_popupLayout
- */
-public class LatinKeyboardBaseView extends View implements PointerTracker.UIProxy {
- private static final String TAG = "LatinKeyboardBaseView";
- private static final boolean DEBUG = false;
-
- public static final int NOT_A_TOUCH_COORDINATE = -1;
-
- public interface OnKeyboardActionListener {
-
- /**
- * Called when the user presses a key. This is sent before the
- * {@link #onKey} is called. For keys that repeat, this is only
- * called once.
- *
- * @param primaryCode
- * the unicode of the key being pressed. If the touch is
- * not on a valid key, the value will be zero.
- */
- void onPress(int primaryCode);
-
- /**
- * Called when the user releases a key. This is sent after the
- * {@link #onKey} is called. For keys that repeat, this is only
- * called once.
- *
- * @param primaryCode
- * the code of the key that was released
- */
- void onRelease(int primaryCode);
-
- /**
- * Send a key press to the listener.
- *
- * @param primaryCode
- * this is the key that was pressed
- * @param keyCodes
- * the codes for all the possible alternative keys with
- * the primary code being the first. If the primary key
- * code is a single character such as an alphabet or
- * number or symbol, the alternatives will include other
- * characters that may be on the same key or adjacent
- * keys. These codes are useful to correct for
- * accidental presses of a key adjacent to the intended
- * key.
- * @param x
- * x-coordinate pixel of touched event. If onKey is not called by onTouchEvent,
- * the value should be NOT_A_TOUCH_COORDINATE.
- * @param y
- * y-coordinate pixel of touched event. If onKey is not called by onTouchEvent,
- * the value should be NOT_A_TOUCH_COORDINATE.
- */
- void onKey(int primaryCode, int[] keyCodes, int x, int y);
-
- /**
- * Sends a sequence of characters to the listener.
- *
- * @param text
- * the sequence of characters to be displayed.
- */
- void onText(CharSequence text);
-
- /**
- * Called when user released a finger outside any key.
- */
- void onCancel();
-
- /**
- * Called when the user quickly moves the finger from right to
- * left.
- */
- void swipeLeft();
-
- /**
- * Called when the user quickly moves the finger from left to
- * right.
- */
- void swipeRight();
-
- /**
- * Called when the user quickly moves the finger from up to down.
- */
- void swipeDown();
-
- /**
- * Called when the user quickly moves the finger from down to up.
- */
- void swipeUp();
- }
-
- // Timing constants
- private final int mKeyRepeatInterval;
-
- // Miscellaneous constants
- /* package */ static final int NOT_A_KEY = -1;
- private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
- private static final int NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL = -1;
-
- // XML attribute
- private int mKeyTextSize;
- private int mKeyTextColor;
- private Typeface mKeyTextStyle = Typeface.DEFAULT;
- private int mLabelTextSize;
- private int mSymbolColorScheme = 0;
- private int mShadowColor;
- private float mShadowRadius;
- private Drawable mKeyBackground;
- private float mBackgroundDimAmount;
- private float mKeyHysteresisDistance;
- private float mVerticalCorrection;
- private int mPreviewOffset;
- private int mPreviewHeight;
- private int mPopupLayout;
-
- // Main keyboard
- private Keyboard mKeyboard;
- private Key[] mKeys;
- // TODO this attribute should be gotten from Keyboard.
- private int mKeyboardVerticalGap;
-
- // Key preview popup
- private TextView mPreviewText;
- private PopupWindow mPreviewPopup;
- private int mPreviewTextSizeLarge;
- private int[] mOffsetInWindow;
- private int mOldPreviewKeyIndex = NOT_A_KEY;
- private boolean mShowPreview = true;
- private boolean mShowTouchPoints = true;
- private int mPopupPreviewOffsetX;
- private int mPopupPreviewOffsetY;
- private int mWindowY;
- private int mPopupPreviewDisplayedY;
- private final int mDelayBeforePreview;
- private final int mDelayAfterPreview;
-
- // Popup mini keyboard
- private PopupWindow mMiniKeyboardPopup;
- private LatinKeyboardBaseView mMiniKeyboard;
- private View mMiniKeyboardParent;
- private final WeakHashMap<Key, View> mMiniKeyboardCache = new WeakHashMap<Key, View>();
- private int mMiniKeyboardOriginX;
- private int mMiniKeyboardOriginY;
- private long mMiniKeyboardPopupTime;
- private int[] mWindowOffset;
- private final float mMiniKeyboardSlideAllowance;
- private int mMiniKeyboardTrackerId;
-
- /** Listener for {@link OnKeyboardActionListener}. */
- private OnKeyboardActionListener mKeyboardActionListener;
-
- private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
-
- // TODO: Let the PointerTracker class manage this pointer queue
- private final PointerQueue mPointerQueue = new PointerQueue();
-
- private final boolean mHasDistinctMultitouch;
- private int mOldPointerCount = 1;
-
- protected KeyDetector mKeyDetector = new ProximityKeyDetector();
-
- // Swipe gesture detector
- private GestureDetector mGestureDetector;
- private final SwipeTracker mSwipeTracker = new SwipeTracker();
- private final int mSwipeThreshold;
- private final boolean mDisambiguateSwipe;
-
- // Drawing
- /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
- private boolean mDrawPending;
- /** The dirty region in the keyboard bitmap */
- private final Rect mDirtyRect = new Rect();
- /** The keyboard bitmap for faster updates */
- private Bitmap mBuffer;
- /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
- private boolean mKeyboardChanged;
- private Key mInvalidatedKey;
- /** The canvas for the above mutable keyboard bitmap */
- private Canvas mCanvas;
- private final Paint mPaint;
- private final Rect mPadding;
- private final Rect mClipRegion = new Rect(0, 0, 0, 0);
- // This map caches key label text height in pixel as value and key label text size as map key.
- private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
- // Distance from horizontal center of the key, proportional to key label text height.
- private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR = 0.55f;
- private final String KEY_LABEL_HEIGHT_REFERENCE_CHAR = "H";
-
- private final UIHandler mHandler = new UIHandler();
-
- class UIHandler extends Handler {
- private static final int MSG_POPUP_PREVIEW = 1;
- private static final int MSG_DISMISS_PREVIEW = 2;
- private static final int MSG_REPEAT_KEY = 3;
- private static final int MSG_LONGPRESS_KEY = 4;
-
- private boolean mInKeyRepeat;
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_POPUP_PREVIEW:
- showKey(msg.arg1, (PointerTracker)msg.obj);
- break;
- case MSG_DISMISS_PREVIEW:
- mPreviewPopup.dismiss();
- break;
- case MSG_REPEAT_KEY: {
- final PointerTracker tracker = (PointerTracker)msg.obj;
- tracker.repeatKey(msg.arg1);
- startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker);
- break;
- }
- case MSG_LONGPRESS_KEY: {
- final PointerTracker tracker = (PointerTracker)msg.obj;
- openPopupIfRequired(msg.arg1, tracker);
- break;
- }
- }
- }
-
- public void popupPreview(long delay, int keyIndex, PointerTracker tracker) {
- removeMessages(MSG_POPUP_PREVIEW);
- if (mPreviewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
- // Show right away, if it's already visible and finger is moving around
- showKey(keyIndex, tracker);
- } else {
- sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0, tracker),
- delay);
- }
- }
-
- public void cancelPopupPreview() {
- removeMessages(MSG_POPUP_PREVIEW);
- }
-
- public void dismissPreview(long delay) {
- if (mPreviewPopup.isShowing()) {
- sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay);
- }
- }
-
- public void cancelDismissPreview() {
- removeMessages(MSG_DISMISS_PREVIEW);
- }
-
- public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
- mInKeyRepeat = true;
- sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
- }
-
- public void cancelKeyRepeatTimer() {
- mInKeyRepeat = false;
- removeMessages(MSG_REPEAT_KEY);
- }
-
- public boolean isInKeyRepeat() {
- return mInKeyRepeat;
- }
-
- public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
- removeMessages(MSG_LONGPRESS_KEY);
- sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
- }
-
- public void cancelLongPressTimer() {
- removeMessages(MSG_LONGPRESS_KEY);
- }
-
- public void cancelKeyTimers() {
- cancelKeyRepeatTimer();
- cancelLongPressTimer();
- }
-
- public void cancelAllMessages() {
- cancelKeyTimers();
- cancelPopupPreview();
- cancelDismissPreview();
- }
- }
-
- static class PointerQueue {
- private LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
-
- public void add(PointerTracker tracker) {
- mQueue.add(tracker);
- }
-
- public int lastIndexOf(PointerTracker tracker) {
- LinkedList<PointerTracker> queue = mQueue;
- for (int index = queue.size() - 1; index >= 0; index--) {
- PointerTracker t = queue.get(index);
- if (t == tracker)
- return index;
- }
- return -1;
- }
-
- public void releaseAllPointersOlderThan(PointerTracker tracker, long eventTime) {
- LinkedList<PointerTracker> queue = mQueue;
- int oldestPos = 0;
- for (PointerTracker t = queue.get(oldestPos); t != tracker; t = queue.get(oldestPos)) {
- if (t.isModifier()) {
- oldestPos++;
- } else {
- t.onUpEvent(t.getLastX(), t.getLastY(), eventTime);
- t.setAlreadyProcessed();
- queue.remove(oldestPos);
- }
- }
- }
-
- public void releaseAllPointersExcept(PointerTracker tracker, long eventTime) {
- for (PointerTracker t : mQueue) {
- if (t == tracker)
- continue;
- t.onUpEvent(t.getLastX(), t.getLastY(), eventTime);
- t.setAlreadyProcessed();
- }
- mQueue.clear();
- if (tracker != null)
- mQueue.add(tracker);
- }
-
- public void remove(PointerTracker tracker) {
- mQueue.remove(tracker);
- }
-
- public boolean isInSlidingKeyInput() {
- for (final PointerTracker tracker : mQueue) {
- if (tracker.isInSlidingKeyInput())
- return true;
- }
- return false;
- }
- }
-
- public LatinKeyboardBaseView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.keyboardViewStyle);
- }
-
- public LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.LatinKeyboardBaseView, defStyle, R.style.LatinKeyboardBaseView);
- LayoutInflater inflate =
- (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- int previewLayout = 0;
- int keyTextSize = 0;
-
- int n = a.getIndexCount();
-
- for (int i = 0; i < n; i++) {
- int attr = a.getIndex(i);
-
- switch (attr) {
- case R.styleable.LatinKeyboardBaseView_keyBackground:
- mKeyBackground = a.getDrawable(attr);
- break;
- case R.styleable.LatinKeyboardBaseView_keyHysteresisDistance:
- mKeyHysteresisDistance = a.getDimensionPixelOffset(attr, 0);
- break;
- case R.styleable.LatinKeyboardBaseView_verticalCorrection:
- mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
- break;
- case R.styleable.LatinKeyboardBaseView_keyPreviewLayout:
- previewLayout = a.getResourceId(attr, 0);
- break;
- case R.styleable.LatinKeyboardBaseView_keyPreviewOffset:
- mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
- break;
- case R.styleable.LatinKeyboardBaseView_keyPreviewHeight:
- mPreviewHeight = a.getDimensionPixelSize(attr, 80);
- break;
- case R.styleable.LatinKeyboardBaseView_keyTextSize:
- mKeyTextSize = a.getDimensionPixelSize(attr, 18);
- break;
- case R.styleable.LatinKeyboardBaseView_keyTextColor:
- mKeyTextColor = a.getColor(attr, 0xFF000000);
- break;
- case R.styleable.LatinKeyboardBaseView_labelTextSize:
- mLabelTextSize = a.getDimensionPixelSize(attr, 14);
- break;
- case R.styleable.LatinKeyboardBaseView_popupLayout:
- mPopupLayout = a.getResourceId(attr, 0);
- break;
- case R.styleable.LatinKeyboardBaseView_shadowColor:
- mShadowColor = a.getColor(attr, 0);
- break;
- case R.styleable.LatinKeyboardBaseView_shadowRadius:
- mShadowRadius = a.getFloat(attr, 0f);
- break;
- // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount)
- case R.styleable.LatinKeyboardBaseView_backgroundDimAmount:
- mBackgroundDimAmount = a.getFloat(attr, 0.5f);
- break;
- //case android.R.styleable.
- case R.styleable.LatinKeyboardBaseView_keyTextStyle:
- int textStyle = a.getInt(attr, 0);
- switch (textStyle) {
- case 0:
- mKeyTextStyle = Typeface.DEFAULT;
- break;
- case 1:
- mKeyTextStyle = Typeface.DEFAULT_BOLD;
- break;
- default:
- mKeyTextStyle = Typeface.defaultFromStyle(textStyle);
- break;
- }
- break;
- case R.styleable.LatinKeyboardBaseView_symbolColorScheme:
- mSymbolColorScheme = a.getInt(attr, 0);
- break;
- }
- }
-
- final Resources res = getResources();
-
- mPreviewPopup = new PopupWindow(context);
- if (previewLayout != 0) {
- mPreviewText = (TextView) inflate.inflate(previewLayout, null);
- mPreviewTextSizeLarge = (int) res.getDimension(R.dimen.key_preview_text_size_large);
- mPreviewPopup.setContentView(mPreviewText);
- mPreviewPopup.setBackgroundDrawable(null);
- } else {
- mShowPreview = false;
- }
- mPreviewPopup.setTouchable(false);
- mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation);
- mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
- mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
-
- mMiniKeyboardParent = this;
- mMiniKeyboardPopup = new PopupWindow(context);
- mMiniKeyboardPopup.setBackgroundDrawable(null);
- mMiniKeyboardPopup.setAnimationStyle(R.style.MiniKeyboardAnimation);
-
- mPaint = new Paint();
- mPaint.setAntiAlias(true);
- mPaint.setTextSize(keyTextSize);
- mPaint.setTextAlign(Align.CENTER);
- mPaint.setAlpha(255);
-
- mPadding = new Rect(0, 0, 0, 0);
- mKeyBackground.getPadding(mPadding);
-
- mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
- // TODO: Refer frameworks/base/core/res/res/values/config.xml
- mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
- mMiniKeyboardSlideAllowance = res.getDimension(R.dimen.mini_keyboard_slide_allowance);
-
- GestureDetector.SimpleOnGestureListener listener =
- new GestureDetector.SimpleOnGestureListener() {
- @Override
- public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
- float velocityY) {
- final float absX = Math.abs(velocityX);
- final float absY = Math.abs(velocityY);
- float deltaX = me2.getX() - me1.getX();
- float deltaY = me2.getY() - me1.getY();
- int travelX = getWidth() / 2; // Half the keyboard width
- int travelY = getHeight() / 2; // Half the keyboard height
- mSwipeTracker.computeCurrentVelocity(1000);
- final float endingVelocityX = mSwipeTracker.getXVelocity();
- final float endingVelocityY = mSwipeTracker.getYVelocity();
- if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) {
- if (mDisambiguateSwipe && endingVelocityX >= velocityX / 4) {
- swipeRight();
- return true;
- }
- } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) {
- if (mDisambiguateSwipe && endingVelocityX <= velocityX / 4) {
- swipeLeft();
- return true;
- }
- } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) {
- if (mDisambiguateSwipe && endingVelocityY <= velocityY / 4) {
- swipeUp();
- return true;
- }
- } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
- if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
- swipeDown();
- return true;
- }
- }
- return false;
- }
- };
-
- final boolean ignoreMultitouch = true;
- mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
- mGestureDetector.setIsLongpressEnabled(false);
-
- mHasDistinctMultitouch = context.getPackageManager()
- .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
- mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
- }
-
- public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
- mKeyboardActionListener = listener;
- for (PointerTracker tracker : mPointerTrackers) {
- tracker.setOnKeyboardActionListener(listener);
- }
- }
-
- /**
- * Returns the {@link OnKeyboardActionListener} object.
- * @return the listener attached to this keyboard
- */
- protected OnKeyboardActionListener getOnKeyboardActionListener() {
- return mKeyboardActionListener;
- }
-
- /**
- * Attaches a keyboard to this view. The keyboard can be switched at any time and the
- * view will re-layout itself to accommodate the keyboard.
- * @see Keyboard
- * @see #getKeyboard()
- * @param keyboard the keyboard to display in this view
- */
- public void setKeyboard(Keyboard keyboard) {
- if (mKeyboard != null) {
- dismissKeyPreview();
- }
- // Remove any pending messages, except dismissing preview
- mHandler.cancelKeyTimers();
- mHandler.cancelPopupPreview();
- mKeyboard = keyboard;
- LatinImeLogger.onSetKeyboard(keyboard);
- mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
- -getPaddingTop() + mVerticalCorrection);
- mKeyboardVerticalGap = (int)getResources().getDimension(R.dimen.key_bottom_gap);
- for (PointerTracker tracker : mPointerTrackers) {
- tracker.setKeyboard(mKeys, mKeyHysteresisDistance);
- }
- requestLayout();
- // Hint to reallocate the buffer if the size changed
- mKeyboardChanged = true;
- invalidateAllKeys();
- computeProximityThreshold(keyboard);
- mMiniKeyboardCache.clear();
- }
-
- /**
- * Returns the current keyboard being displayed by this view.
- * @return the currently attached keyboard
- * @see #setKeyboard(Keyboard)
- */
- public Keyboard getKeyboard() {
- return mKeyboard;
- }
-
- /**
- * Return whether the device has distinct multi-touch panel.
- * @return true if the device has distinct multi-touch panel.
- */
- public boolean hasDistinctMultitouch() {
- return mHasDistinctMultitouch;
- }
-
- /**
- * Sets the state of the shift key of the keyboard, if any.
- * @param shifted whether or not to enable the state of the shift key
- * @return true if the shift key state changed, false if there was no change
- */
- public boolean setShifted(boolean shifted) {
- if (mKeyboard != null) {
- if (mKeyboard.setShifted(shifted)) {
- // The whole keyboard probably needs to be redrawn
- invalidateAllKeys();
- return true;
- }
- }
- return false;
- }
-
- /**
- * Returns the state of the shift key of the keyboard, if any.
- * @return true if the shift is in a pressed state, false otherwise. If there is
- * no shift key on the keyboard or there is no keyboard attached, it returns false.
- */
- public boolean isShifted() {
- if (mKeyboard != null) {
- return mKeyboard.isShifted();
- }
- return false;
- }
-
- /**
- * Enables or disables the key feedback popup. This is a popup that shows a magnified
- * version of the depressed key. By default the preview is enabled.
- * @param previewEnabled whether or not to enable the key feedback popup
- * @see #isPreviewEnabled()
- */
- public void setPreviewEnabled(boolean previewEnabled) {
- mShowPreview = previewEnabled;
- }
-
- /**
- * Returns the enabled state of the key feedback popup.
- * @return whether or not the key feedback popup is enabled
- * @see #setPreviewEnabled(boolean)
- */
- public boolean isPreviewEnabled() {
- return mShowPreview;
- }
-
- public int getSymbolColorScheme() {
- return mSymbolColorScheme;
- }
-
- public void setPopupParent(View v) {
- mMiniKeyboardParent = v;
- }
-
- public void setPopupOffset(int x, int y) {
- mPopupPreviewOffsetX = x;
- mPopupPreviewOffsetY = y;
- mPreviewPopup.dismiss();
- }
-
- /**
- * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
- * codes for adjacent keys. When disabled, only the primary key code will be
- * reported.
- * @param enabled whether or not the proximity correction is enabled
- */
- public void setProximityCorrectionEnabled(boolean enabled) {
- mKeyDetector.setProximityCorrectionEnabled(enabled);
- }
-
- /**
- * Returns true if proximity correction is enabled.
- */
- public boolean isProximityCorrectionEnabled() {
- return mKeyDetector.isProximityCorrectionEnabled();
- }
-
- protected Locale getKeyboardLocale() {
- if (mKeyboard instanceof LatinKeyboard) {
- return ((LatinKeyboard)mKeyboard).getInputLocale();
- } else {
- return getContext().getResources().getConfiguration().locale;
- }
- }
-
- protected CharSequence adjustCase(CharSequence label) {
- if (mKeyboard.isShifted() && label != null && label.length() < 3
- && Character.isLowerCase(label.charAt(0))) {
- return label.toString().toUpperCase(getKeyboardLocale());
- }
- return label;
- }
-
- @Override
- public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // Round up a little
- if (mKeyboard == null) {
- setMeasuredDimension(
- getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
- } else {
- int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
- if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
- width = MeasureSpec.getSize(widthMeasureSpec);
- }
- setMeasuredDimension(
- width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
- }
- }
-
- /**
- * Compute the average distance between adjacent keys (horizontally and vertically)
- * and square it to get the proximity threshold. We use a square here and in computing
- * the touch distance from a key's center to avoid taking a square root.
- * @param keyboard
- */
- private void computeProximityThreshold(Keyboard keyboard) {
- if (keyboard == null) return;
- final Key[] keys = mKeys;
- if (keys == null) return;
- int length = keys.length;
- int dimensionSum = 0;
- for (int i = 0; i < length; i++) {
- Key key = keys[i];
- dimensionSum += Math.min(key.width, key.height + mKeyboardVerticalGap) + key.gap;
- }
- if (dimensionSum < 0 || length == 0) return;
- mKeyDetector.setProximityThreshold((int) (dimensionSum * 1.4f / length));
- }
-
- @Override
- public void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- // Release the buffer, if any and it will be reallocated on the next draw
- mBuffer = null;
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- if (mDrawPending || mBuffer == null || mKeyboardChanged) {
- onBufferDraw();
- }
- canvas.drawBitmap(mBuffer, 0, 0, null);
- }
-
- private void onBufferDraw() {
- if (mBuffer == null || mKeyboardChanged) {
- if (mBuffer == null || mKeyboardChanged &&
- (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
- // Make sure our bitmap is at least 1x1
- final int width = Math.max(1, getWidth());
- final int height = Math.max(1, getHeight());
- mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- mCanvas = new Canvas(mBuffer);
- }
- invalidateAllKeys();
- mKeyboardChanged = false;
- }
- final Canvas canvas = mCanvas;
- canvas.clipRect(mDirtyRect, Op.REPLACE);
-
- if (mKeyboard == null) return;
-
- final Paint paint = mPaint;
- final Drawable keyBackground = mKeyBackground;
- final Rect clipRegion = mClipRegion;
- final Rect padding = mPadding;
- final int kbdPaddingLeft = getPaddingLeft();
- final int kbdPaddingTop = getPaddingTop();
- final Key[] keys = mKeys;
- final Key invalidKey = mInvalidatedKey;
-
- paint.setColor(mKeyTextColor);
- boolean drawSingleKey = false;
- if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
- // TODO we should use Rect.inset and Rect.contains here.
- // Is clipRegion completely contained within the invalidated key?
- if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
- invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
- invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
- invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
- drawSingleKey = true;
- }
- }
- canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
- final int keyCount = keys.length;
- for (int i = 0; i < keyCount; i++) {
- final Key key = keys[i];
- if (drawSingleKey && invalidKey != key) {
- continue;
- }
- int[] drawableState = key.getCurrentDrawableState();
- keyBackground.setState(drawableState);
-
- // Switch the character to uppercase if shift is pressed
- String label = key.label == null? null : adjustCase(key.label).toString();
-
- final Rect bounds = keyBackground.getBounds();
- if (key.width != bounds.right || key.height != bounds.bottom) {
- keyBackground.setBounds(0, 0, key.width, key.height);
- }
- canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
- keyBackground.draw(canvas);
-
- boolean shouldDrawIcon = true;
- if (label != null) {
- // For characters, use large font. For labels like "Done", use small font.
- final int labelSize;
- if (label.length() > 1 && key.codes.length < 2) {
- labelSize = mLabelTextSize;
- paint.setTypeface(Typeface.DEFAULT_BOLD);
- } else {
- labelSize = mKeyTextSize;
- paint.setTypeface(mKeyTextStyle);
- }
- paint.setTextSize(labelSize);
-
- Integer labelHeightValue = mTextHeightCache.get(labelSize);
- final int labelHeight;
- if (labelHeightValue != null) {
- labelHeight = labelHeightValue;
- } else {
- Rect textBounds = new Rect();
- paint.getTextBounds(KEY_LABEL_HEIGHT_REFERENCE_CHAR, 0, 1, textBounds);
- labelHeight = textBounds.height();
- mTextHeightCache.put(labelSize, labelHeight);
- }
-
- // Draw a drop shadow for the text
- paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
- final int centerX = (key.width + padding.left - padding.right) / 2;
- final int centerY = (key.height + padding.top - padding.bottom) / 2;
- final float baseline = centerY
- + labelHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR;
- canvas.drawText(label, centerX, baseline, paint);
- // Turn off drop shadow
- paint.setShadowLayer(0, 0, 0, 0);
-
- // Usually don't draw icon if label is not null, but we draw icon for the number
- // hint and popup hint.
- shouldDrawIcon = shouldDrawLabelAndIcon(key);
- }
- if (key.icon != null && shouldDrawIcon) {
- // Special handing for the upper-right number hint icons
- final int drawableWidth;
- final int drawableHeight;
- final int drawableX;
- final int drawableY;
- if (shouldDrawIconFully(key)) {
- drawableWidth = key.width;
- drawableHeight = key.height;
- drawableX = 0;
- drawableY = NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL;
- } else {
- drawableWidth = key.icon.getIntrinsicWidth();
- drawableHeight = key.icon.getIntrinsicHeight();
- drawableX = (key.width + padding.left - padding.right - drawableWidth) / 2;
- drawableY = (key.height + padding.top - padding.bottom - drawableHeight) / 2;
- }
- canvas.translate(drawableX, drawableY);
- key.icon.setBounds(0, 0, drawableWidth, drawableHeight);
- key.icon.draw(canvas);
- canvas.translate(-drawableX, -drawableY);
- }
- canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
- }
- mInvalidatedKey = null;
- // Overlay a dark rectangle to dim the keyboard
- if (mMiniKeyboard != null) {
- paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
- canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
- }
-
- if (DEBUG) {
- if (mShowTouchPoints) {
- for (PointerTracker tracker : mPointerTrackers) {
- int startX = tracker.getStartX();
- int startY = tracker.getStartY();
- int lastX = tracker.getLastX();
- int lastY = tracker.getLastY();
- paint.setAlpha(128);
- paint.setColor(0xFFFF0000);
- canvas.drawCircle(startX, startY, 3, paint);
- canvas.drawLine(startX, startY, lastX, lastY, paint);
- paint.setColor(0xFF0000FF);
- canvas.drawCircle(lastX, lastY, 3, paint);
- paint.setColor(0xFF00FF00);
- canvas.drawCircle((startX + lastX) / 2, (startY + lastY) / 2, 2, paint);
- }
- }
- }
-
- mDrawPending = false;
- mDirtyRect.setEmpty();
- }
-
- // TODO: clean up this method.
- private void dismissKeyPreview() {
- for (PointerTracker tracker : mPointerTrackers)
- tracker.updateKey(NOT_A_KEY);
- showPreview(NOT_A_KEY, null);
- }
-
- public void showPreview(int keyIndex, PointerTracker tracker) {
- int oldKeyIndex = mOldPreviewKeyIndex;
- mOldPreviewKeyIndex = keyIndex;
- final boolean isLanguageSwitchEnabled = (mKeyboard instanceof LatinKeyboard)
- && ((LatinKeyboard)mKeyboard).isLanguageSwitchEnabled();
- // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show
- // the space key preview and 3) pointer moves off the space key to other letter key, we
- // should hide the preview of the previous key.
- final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null)
- || tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex);
- // If key changed and preview is on or the key is space (language switch is enabled)
- if (oldKeyIndex != keyIndex
- && (mShowPreview
- || (hidePreviewOrShowSpaceKeyPreview && isLanguageSwitchEnabled))) {
- if (keyIndex == NOT_A_KEY) {
- mHandler.cancelPopupPreview();
- mHandler.dismissPreview(mDelayAfterPreview);
- } else if (tracker != null) {
- mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker);
- }
- }
- }
-
- private void showKey(final int keyIndex, PointerTracker tracker) {
- Key key = tracker.getKey(keyIndex);
- if (key == null)
- return;
- // Should not draw hint icon in key preview
- if (key.icon != null && !shouldDrawLabelAndIcon(key)) {
- mPreviewText.setCompoundDrawables(null, null, null,
- key.iconPreview != null ? key.iconPreview : key.icon);
- mPreviewText.setText(null);
- } else {
- mPreviewText.setCompoundDrawables(null, null, null, null);
- mPreviewText.setText(adjustCase(tracker.getPreviewText(key)));
- if (key.label.length() > 1 && key.codes.length < 2) {
- mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
- mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
- } else {
- mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
- mPreviewText.setTypeface(mKeyTextStyle);
- }
- }
- mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
- + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
- final int popupHeight = mPreviewHeight;
- LayoutParams lp = mPreviewText.getLayoutParams();
- if (lp != null) {
- lp.width = popupWidth;
- lp.height = popupHeight;
- }
-
- int popupPreviewX = key.x - (popupWidth - key.width) / 2;
- int popupPreviewY = key.y - popupHeight + mPreviewOffset;
-
- mHandler.cancelDismissPreview();
- if (mOffsetInWindow == null) {
- mOffsetInWindow = new int[2];
- getLocationInWindow(mOffsetInWindow);
- mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero
- mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero
- int[] windowLocation = new int[2];
- getLocationOnScreen(windowLocation);
- mWindowY = windowLocation[1];
- }
- // Set the preview background state
- mPreviewText.getBackground().setState(
- key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
- popupPreviewX += mOffsetInWindow[0];
- popupPreviewY += mOffsetInWindow[1];
-
- // If the popup cannot be shown above the key, put it on the side
- if (popupPreviewY + mWindowY < 0) {
- // If the key you're pressing is on the left side of the keyboard, show the popup on
- // the right, offset by enough to see at least one key to the left/right.
- if (key.x + key.width <= getWidth() / 2) {
- popupPreviewX += (int) (key.width * 2.5);
- } else {
- popupPreviewX -= (int) (key.width * 2.5);
- }
- popupPreviewY += popupHeight;
- }
-
- if (mPreviewPopup.isShowing()) {
- mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight);
- } else {
- mPreviewPopup.setWidth(popupWidth);
- mPreviewPopup.setHeight(popupHeight);
- mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY,
- popupPreviewX, popupPreviewY);
- }
- // Record popup preview position to display mini-keyboard later at the same positon
- mPopupPreviewDisplayedY = popupPreviewY;
- mPreviewText.setVisibility(VISIBLE);
- }
-
- /**
- * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
- * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
- * draws the cached buffer.
- * @see #invalidateKey(Key)
- */
- public void invalidateAllKeys() {
- mDirtyRect.union(0, 0, getWidth(), getHeight());
- mDrawPending = true;
- invalidate();
- }
-
- /**
- * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
- * one key is changing it's content. Any changes that affect the position or size of the key
- * may not be honored.
- * @param key key in the attached {@link Keyboard}.
- * @see #invalidateAllKeys
- */
- public void invalidateKey(Key key) {
- if (key == null)
- return;
- mInvalidatedKey = key;
- // TODO we should clean up this and record key's region to use in onBufferDraw.
- mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(),
- key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
- onBufferDraw();
- invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(),
- key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
- }
-
- private boolean openPopupIfRequired(int keyIndex, PointerTracker tracker) {
- // Check if we have a popup layout specified first.
- if (mPopupLayout == 0) {
- return false;
- }
-
- Key popupKey = tracker.getKey(keyIndex);
- if (popupKey == null)
- return false;
- boolean result = onLongPress(popupKey);
- if (result) {
- dismissKeyPreview();
- mMiniKeyboardTrackerId = tracker.mPointerId;
- // Mark this tracker "already processed" and remove it from the pointer queue
- tracker.setAlreadyProcessed();
- mPointerQueue.remove(tracker);
- }
- return result;
- }
-
- private View inflateMiniKeyboardContainer(Key popupKey) {
- int popupKeyboardId = popupKey.popupResId;
- LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(
- Context.LAYOUT_INFLATER_SERVICE);
- View container = inflater.inflate(mPopupLayout, null);
- if (container == null)
- throw new NullPointerException();
-
- LatinKeyboardBaseView miniKeyboard =
- (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView);
- miniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
- public void onKey(int primaryCode, int[] keyCodes, int x, int y) {
- mKeyboardActionListener.onKey(primaryCode, keyCodes, x, y);
- dismissPopupKeyboard();
- }
-
- public void onText(CharSequence text) {
- mKeyboardActionListener.onText(text);
- dismissPopupKeyboard();
- }
-
- public void onCancel() {
- mKeyboardActionListener.onCancel();
- dismissPopupKeyboard();
- }
-
- public void swipeLeft() {
- }
- public void swipeRight() {
- }
- public void swipeUp() {
- }
- public void swipeDown() {
- }
- public void onPress(int primaryCode) {
- mKeyboardActionListener.onPress(primaryCode);
- }
- public void onRelease(int primaryCode) {
- mKeyboardActionListener.onRelease(primaryCode);
- }
- });
- // Override default ProximityKeyDetector.
- miniKeyboard.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance);
- // Remove gesture detector on mini-keyboard
- miniKeyboard.mGestureDetector = null;
-
- Keyboard keyboard;
- if (popupKey.popupCharacters != null) {
- keyboard = new Keyboard(getContext(), popupKeyboardId, popupKey.popupCharacters,
- -1, getPaddingLeft() + getPaddingRight());
- } else {
- keyboard = new Keyboard(getContext(), popupKeyboardId);
- }
- miniKeyboard.setKeyboard(keyboard);
- miniKeyboard.setPopupParent(this);
-
- container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
- MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
-
- return container;
- }
-
- private static boolean isOneRowKeys(List<Key> keys) {
- if (keys.size() == 0) return false;
- final int edgeFlags = keys.get(0).edgeFlags;
- // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows,
- // does not have both top and bottom edge flags on at the same time. On the other hand,
- // the first key of mini keyboard that was created with popupCharacters must have both top
- // and bottom edge flags on.
- // When you want to use one row mini-keyboard from xml file, make sure that the row has
- // both top and bottom edge flags set.
- return (edgeFlags & Keyboard.EDGE_TOP) != 0 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0;
- }
-
- /**
- * Called when a key is long pressed. By default this will open any popup keyboard associated
- * with this key through the attributes popupLayout and popupCharacters.
- * @param popupKey the key that was long pressed
- * @return true if the long press is handled, false otherwise. Subclasses should call the
- * method on the base class if the subclass doesn't wish to handle the call.
- */
- protected boolean onLongPress(Key popupKey) {
- // TODO if popupKey.popupCharacters has only one letter, send it as key without opening
- // mini keyboard.
-
- if (popupKey.popupResId == 0)
- return false;
-
- View container = mMiniKeyboardCache.get(popupKey);
- if (container == null) {
- container = inflateMiniKeyboardContainer(popupKey);
- mMiniKeyboardCache.put(popupKey, container);
- }
- mMiniKeyboard = (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView);
- if (mWindowOffset == null) {
- mWindowOffset = new int[2];
- getLocationInWindow(mWindowOffset);
- }
-
- // Get width of a key in the mini popup keyboard = "miniKeyWidth".
- // On the other hand, "popupKey.width" is width of the pressed key on the main keyboard.
- // We adjust the position of mini popup keyboard with the edge key in it:
- // a) When we have the leftmost key in popup keyboard directly above the pressed key
- // Right edges of both keys should be aligned for consistent default selection
- // b) When we have the rightmost key in popup keyboard directly above the pressed key
- // Left edges of both keys should be aligned for consistent default selection
- final List<Key> miniKeys = mMiniKeyboard.getKeyboard().getKeys();
- final int miniKeyWidth = miniKeys.size() > 0 ? miniKeys.get(0).width : 0;
-
- // HACK: Have the leftmost number in the popup characters right above the key
- boolean isNumberAtLeftmost =
- hasMultiplePopupChars(popupKey) && isNumberAtLeftmostPopupChar(popupKey);
- int popupX = popupKey.x + mWindowOffset[0];
- popupX += getPaddingLeft();
- if (isNumberAtLeftmost) {
- popupX += popupKey.width - miniKeyWidth; // adjustment for a) described above
- popupX -= container.getPaddingLeft();
- } else {
- popupX += miniKeyWidth; // adjustment for b) described above
- popupX -= container.getMeasuredWidth();
- popupX += container.getPaddingRight();
- }
- int popupY = popupKey.y + mWindowOffset[1];
- popupY += getPaddingTop();
- popupY -= container.getMeasuredHeight();
- popupY += container.getPaddingBottom();
- final int x = popupX;
- final int y = mShowPreview && isOneRowKeys(miniKeys) ? mPopupPreviewDisplayedY : popupY;
-
- int adjustedX = x;
- if (x < 0) {
- adjustedX = 0;
- } else if (x > (getMeasuredWidth() - container.getMeasuredWidth())) {
- adjustedX = getMeasuredWidth() - container.getMeasuredWidth();
- }
- mMiniKeyboardOriginX = adjustedX + container.getPaddingLeft() - mWindowOffset[0];
- mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1];
- mMiniKeyboard.setPopupOffset(adjustedX, y);
- mMiniKeyboard.setShifted(isShifted());
- // Mini keyboard needs no pop-up key preview displayed.
- mMiniKeyboard.setPreviewEnabled(false);
- mMiniKeyboardPopup.setContentView(container);
- mMiniKeyboardPopup.setWidth(container.getMeasuredWidth());
- mMiniKeyboardPopup.setHeight(container.getMeasuredHeight());
- mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
-
- // Inject down event on the key to mini keyboard.
- long eventTime = SystemClock.uptimeMillis();
- mMiniKeyboardPopupTime = eventTime;
- MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN, popupKey.x
- + popupKey.width / 2, popupKey.y + popupKey.height / 2, eventTime);
- mMiniKeyboard.onTouchEvent(downEvent);
- downEvent.recycle();
-
- invalidateAllKeys();
- return true;
- }
-
- private static boolean hasMultiplePopupChars(Key key) {
- if (key.popupCharacters != null && key.popupCharacters.length() > 1) {
- return true;
- }
- return false;
- }
-
- private boolean shouldDrawIconFully(Key key) {
- return isNumberAtEdgeOfPopupChars(key) || isLatinF1Key(key)
- || LatinKeyboard.hasPuncOrSmileysPopup(key);
- }
-
- private boolean shouldDrawLabelAndIcon(Key key) {
- return isNumberAtEdgeOfPopupChars(key) || isNonMicLatinF1Key(key)
- || LatinKeyboard.hasPuncOrSmileysPopup(key);
- }
-
- private boolean isLatinF1Key(Key key) {
- return (mKeyboard instanceof LatinKeyboard) && ((LatinKeyboard)mKeyboard).isF1Key(key);
- }
-
- private boolean isNonMicLatinF1Key(Key key) {
- return isLatinF1Key(key) && key.label != null;
- }
-
- private static boolean isNumberAtEdgeOfPopupChars(Key key) {
- return isNumberAtLeftmostPopupChar(key) || isNumberAtRightmostPopupChar(key);
- }
-
- /* package */ static boolean isNumberAtLeftmostPopupChar(Key key) {
- if (key.popupCharacters != null && key.popupCharacters.length() > 0
- && isAsciiDigit(key.popupCharacters.charAt(0))) {
- return true;
- }
- return false;
- }
-
- /* package */ static boolean isNumberAtRightmostPopupChar(Key key) {
- if (key.popupCharacters != null && key.popupCharacters.length() > 0
- && isAsciiDigit(key.popupCharacters.charAt(key.popupCharacters.length() - 1))) {
- return true;
- }
- return false;
- }
-
- private static boolean isAsciiDigit(char c) {
- return (c < 0x80) && Character.isDigit(c);
- }
-
- private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) {
- return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action,
- x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0);
- }
-
- private PointerTracker getPointerTracker(final int id) {
- final ArrayList<PointerTracker> pointers = mPointerTrackers;
- final Key[] keys = mKeys;
- final OnKeyboardActionListener listener = mKeyboardActionListener;
-
- // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
- for (int i = pointers.size(); i <= id; i++) {
- final PointerTracker tracker =
- new PointerTracker(i, mHandler, mKeyDetector, this, getResources());
- if (keys != null)
- tracker.setKeyboard(keys, mKeyHysteresisDistance);
- if (listener != null)
- tracker.setOnKeyboardActionListener(listener);
- pointers.add(tracker);
- }
-
- return pointers.get(id);
- }
-
- public boolean isInSlidingKeyInput() {
- if (mMiniKeyboard != null) {
- return mMiniKeyboard.isInSlidingKeyInput();
- } else {
- return mPointerQueue.isInSlidingKeyInput();
- }
- }
-
- public int getPointerCount() {
- return mOldPointerCount;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent me) {
- final int action = me.getActionMasked();
- final int pointerCount = me.getPointerCount();
- final int oldPointerCount = mOldPointerCount;
- mOldPointerCount = pointerCount;
-
- // TODO: cleanup this code into a multi-touch to single-touch event converter class?
- // If the device does not have distinct multi-touch support panel, ignore all multi-touch
- // events except a transition from/to single-touch.
- if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
- return true;
- }
-
- // Track the last few movements to look for spurious swipes.
- mSwipeTracker.addMovement(me);
-
- // Gesture detector must be enabled only when mini-keyboard is not on the screen.
- if (mMiniKeyboard == null
- && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) {
- dismissKeyPreview();
- mHandler.cancelKeyTimers();
- return true;
- }
-
- final long eventTime = me.getEventTime();
- final int index = me.getActionIndex();
- final int id = me.getPointerId(index);
- final int x = (int)me.getX(index);
- final int y = (int)me.getY(index);
-
- // Needs to be called after the gesture detector gets a turn, as it may have
- // displayed the mini keyboard
- if (mMiniKeyboard != null) {
- final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId);
- if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) {
- final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex);
- final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex);
- MotionEvent translated = generateMiniKeyboardMotionEvent(action,
- miniKeyboardX, miniKeyboardY, eventTime);
- mMiniKeyboard.onTouchEvent(translated);
- translated.recycle();
- }
- return true;
- }
-
- if (mHandler.isInKeyRepeat()) {
- // It will keep being in the key repeating mode while the key is being pressed.
- if (action == MotionEvent.ACTION_MOVE) {
- return true;
- }
- final PointerTracker tracker = getPointerTracker(id);
- // Key repeating timer will be canceled if 2 or more keys are in action, and current
- // event (UP or DOWN) is non-modifier key.
- if (pointerCount > 1 && !tracker.isModifier()) {
- mHandler.cancelKeyRepeatTimer();
- }
- // Up event will pass through.
- }
-
- // TODO: cleanup this code into a multi-touch to single-touch event converter class?
- // Translate mutli-touch event to single-touch events on the device that has no distinct
- // multi-touch panel.
- if (!mHasDistinctMultitouch) {
- // Use only main (id=0) pointer tracker.
- PointerTracker tracker = getPointerTracker(0);
- if (pointerCount == 1 && oldPointerCount == 2) {
- // Multi-touch to single touch transition.
- // Send a down event for the latest pointer.
- tracker.onDownEvent(x, y, eventTime);
- } else if (pointerCount == 2 && oldPointerCount == 1) {
- // Single-touch to multi-touch transition.
- // Send an up event for the last pointer.
- tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime);
- } else if (pointerCount == 1 && oldPointerCount == 1) {
- tracker.onTouchEvent(action, x, y, eventTime);
- } else {
- Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
- + " (old " + oldPointerCount + ")");
- }
- return true;
- }
-
- if (action == MotionEvent.ACTION_MOVE) {
- for (int i = 0; i < pointerCount; i++) {
- PointerTracker tracker = getPointerTracker(me.getPointerId(i));
- tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime);
- }
- } else {
- PointerTracker tracker = getPointerTracker(id);
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_POINTER_DOWN:
- onDownEvent(tracker, x, y, eventTime);
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_POINTER_UP:
- onUpEvent(tracker, x, y, eventTime);
- break;
- case MotionEvent.ACTION_CANCEL:
- onCancelEvent(tracker, x, y, eventTime);
- break;
- }
- }
-
- return true;
- }
-
- private void onDownEvent(PointerTracker tracker, int x, int y, long eventTime) {
- if (tracker.isOnModifierKey(x, y)) {
- // Before processing a down event of modifier key, all pointers already being tracked
- // should be released.
- mPointerQueue.releaseAllPointersExcept(null, eventTime);
- }
- tracker.onDownEvent(x, y, eventTime);
- mPointerQueue.add(tracker);
- }
-
- private void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) {
- if (tracker.isModifier()) {
- // Before processing an up event of modifier key, all pointers already being tracked
- // should be released.
- mPointerQueue.releaseAllPointersExcept(tracker, eventTime);
- } else {
- int index = mPointerQueue.lastIndexOf(tracker);
- if (index >= 0) {
- mPointerQueue.releaseAllPointersOlderThan(tracker, eventTime);
- } else {
- Log.w(TAG, "onUpEvent: corresponding down event not found for pointer "
- + tracker.mPointerId);
- }
- }
- tracker.onUpEvent(x, y, eventTime);
- mPointerQueue.remove(tracker);
- }
-
- private void onCancelEvent(PointerTracker tracker, int x, int y, long eventTime) {
- tracker.onCancelEvent(x, y, eventTime);
- mPointerQueue.remove(tracker);
- }
-
- protected void swipeRight() {
- mKeyboardActionListener.swipeRight();
- }
-
- protected void swipeLeft() {
- mKeyboardActionListener.swipeLeft();
- }
-
- protected void swipeUp() {
- mKeyboardActionListener.swipeUp();
- }
-
- protected void swipeDown() {
- mKeyboardActionListener.swipeDown();
- }
-
- public void closing() {
- mPreviewPopup.dismiss();
- mHandler.cancelAllMessages();
-
- dismissPopupKeyboard();
- mBuffer = null;
- mCanvas = null;
- mMiniKeyboardCache.clear();
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- closing();
- }
-
- private void dismissPopupKeyboard() {
- if (mMiniKeyboardPopup.isShowing()) {
- mMiniKeyboardPopup.dismiss();
- mMiniKeyboard = null;
- mMiniKeyboardOriginX = 0;
- mMiniKeyboardOriginY = 0;
- invalidateAllKeys();
- }
- }
-
- public boolean handleBack() {
- if (mMiniKeyboardPopup.isShowing()) {
- dismissPopupKeyboard();
- return true;
- }
- return false;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
deleted file mode 100644
index efe03a51f..000000000
--- a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * 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.latin;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.inputmethodservice.Keyboard;
-import android.inputmethodservice.Keyboard.Key;
-import android.os.Handler;
-import android.os.Message;
-import android.os.SystemClock;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import java.util.List;
-
-public class LatinKeyboardView extends LatinKeyboardBaseView {
-
- static final int KEYCODE_OPTIONS = -100;
- static final int KEYCODE_OPTIONS_LONGPRESS = -101;
- static final int KEYCODE_VOICE = -102;
- static final int KEYCODE_F1 = -103;
- static final int KEYCODE_NEXT_LANGUAGE = -104;
- static final int KEYCODE_PREV_LANGUAGE = -105;
-
- private Keyboard mPhoneKeyboard;
-
- /** Whether we've started dropping move events because we found a big jump */
- private boolean mDroppingEvents;
- /**
- * Whether multi-touch disambiguation needs to be disabled if a real multi-touch event has
- * occured
- */
- private boolean mDisableDisambiguation;
- /** The distance threshold at which we start treating the touch session as a multi-touch */
- private int mJumpThresholdSquare = Integer.MAX_VALUE;
- /** The y coordinate of the last row */
- private int mLastRowY;
-
- public LatinKeyboardView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- public void setPhoneKeyboard(Keyboard phoneKeyboard) {
- mPhoneKeyboard = phoneKeyboard;
- }
-
- @Override
- public void setPreviewEnabled(boolean previewEnabled) {
- if (getKeyboard() == mPhoneKeyboard) {
- // Phone keyboard never shows popup preview (except language switch).
- super.setPreviewEnabled(false);
- } else {
- super.setPreviewEnabled(previewEnabled);
- }
- }
-
- @Override
- public void setKeyboard(Keyboard newKeyboard) {
- final Keyboard oldKeyboard = getKeyboard();
- if (oldKeyboard instanceof LatinKeyboard) {
- // Reset old keyboard state before switching to new keyboard.
- ((LatinKeyboard)oldKeyboard).keyReleased();
- }
- super.setKeyboard(newKeyboard);
- // One-seventh of the keyboard width seems like a reasonable threshold
- mJumpThresholdSquare = newKeyboard.getMinWidth() / 7;
- mJumpThresholdSquare *= mJumpThresholdSquare;
- // Assuming there are 4 rows, this is the coordinate of the last row
- mLastRowY = (newKeyboard.getHeight() * 3) / 4;
- setKeyboardLocal(newKeyboard);
- }
-
- @Override
- protected boolean onLongPress(Key key) {
- int primaryCode = key.codes[0];
- if (primaryCode == KEYCODE_OPTIONS) {
- return invokeOnKey(KEYCODE_OPTIONS_LONGPRESS);
- } else if (primaryCode == '0' && getKeyboard() == mPhoneKeyboard) {
- // Long pressing on 0 in phone number keypad gives you a '+'.
- return invokeOnKey('+');
- } else {
- return super.onLongPress(key);
- }
- }
-
- private boolean invokeOnKey(int primaryCode) {
- getOnKeyboardActionListener().onKey(primaryCode, null,
- LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE,
- LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE);
- return true;
- }
-
- @Override
- protected CharSequence adjustCase(CharSequence label) {
- Keyboard keyboard = getKeyboard();
- if (keyboard.isShifted()
- && keyboard instanceof LatinKeyboard
- && ((LatinKeyboard) keyboard).isAlphaKeyboard()
- && !TextUtils.isEmpty(label) && label.length() < 3
- && Character.isLowerCase(label.charAt(0))) {
- return label.toString().toUpperCase(getKeyboardLocale());
- }
- return label;
- }
-
- public boolean setShiftLocked(boolean shiftLocked) {
- Keyboard keyboard = getKeyboard();
- if (keyboard instanceof LatinKeyboard) {
- ((LatinKeyboard)keyboard).setShiftLocked(shiftLocked);
- invalidateAllKeys();
- return true;
- }
- return false;
- }
-
- /**
- * This function checks to see if we need to handle any sudden jumps in the pointer location
- * that could be due to a multi-touch being treated as a move by the firmware or hardware.
- * Once a sudden jump is detected, all subsequent move events are discarded
- * until an UP is received.<P>
- * When a sudden jump is detected, an UP event is simulated at the last position and when
- * the sudden moves subside, a DOWN event is simulated for the second key.
- * @param me the motion event
- * @return true if the event was consumed, so that it doesn't continue to be handled by
- * KeyboardView.
- */
- private boolean handleSuddenJump(MotionEvent me) {
- final int action = me.getAction();
- final int x = (int) me.getX();
- final int y = (int) me.getY();
- boolean result = false;
-
- // Real multi-touch event? Stop looking for sudden jumps
- if (me.getPointerCount() > 1) {
- mDisableDisambiguation = true;
- }
- if (mDisableDisambiguation) {
- // If UP, reset the multi-touch flag
- if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false;
- return false;
- }
-
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- // Reset the "session"
- mDroppingEvents = false;
- mDisableDisambiguation = false;
- break;
- case MotionEvent.ACTION_MOVE:
- // Is this a big jump?
- final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y);
- // Check the distance and also if the move is not entirely within the bottom row
- // If it's only in the bottom row, it might be an intentional slide gesture
- // for language switching
- if (distanceSquare > mJumpThresholdSquare
- && (mLastY < mLastRowY || y < mLastRowY)) {
- // If we're not yet dropping events, start dropping and send an UP event
- if (!mDroppingEvents) {
- mDroppingEvents = true;
- // Send an up event
- MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
- MotionEvent.ACTION_UP,
- mLastX, mLastY, me.getMetaState());
- super.onTouchEvent(translated);
- translated.recycle();
- }
- result = true;
- } else if (mDroppingEvents) {
- // If moves are small and we're already dropping events, continue dropping
- result = true;
- }
- break;
- case MotionEvent.ACTION_UP:
- if (mDroppingEvents) {
- // Send a down event first, as we dropped a bunch of sudden jumps and assume that
- // the user is releasing the touch on the second key.
- MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
- MotionEvent.ACTION_DOWN,
- x, y, me.getMetaState());
- super.onTouchEvent(translated);
- translated.recycle();
- mDroppingEvents = false;
- // Let the up event get processed as well, result = false
- }
- break;
- }
- // Track the previous coordinate
- mLastX = x;
- mLastY = y;
- return result;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent me) {
- LatinKeyboard keyboard = (LatinKeyboard) getKeyboard();
- if (DEBUG_LINE) {
- mLastX = (int) me.getX();
- mLastY = (int) me.getY();
- invalidate();
- }
-
- // If there was a sudden jump, return without processing the actual motion event.
- if (handleSuddenJump(me))
- return true;
-
- // Reset any bounding box controls in the keyboard
- if (me.getAction() == MotionEvent.ACTION_DOWN) {
- keyboard.keyReleased();
- }
-
- if (me.getAction() == MotionEvent.ACTION_UP) {
- int languageDirection = keyboard.getLanguageChangeDirection();
- if (languageDirection != 0) {
- getOnKeyboardActionListener().onKey(
- languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE,
- null, mLastX, mLastY);
- me.setAction(MotionEvent.ACTION_CANCEL);
- keyboard.keyReleased();
- return super.onTouchEvent(me);
- }
- }
-
- return super.onTouchEvent(me);
- }
-
- /**************************** INSTRUMENTATION *******************************/
-
- static final boolean DEBUG_AUTO_PLAY = false;
- static final boolean DEBUG_LINE = false;
- private static final int MSG_TOUCH_DOWN = 1;
- private static final int MSG_TOUCH_UP = 2;
-
- Handler mHandler2;
-
- private String mStringToPlay;
- private int mStringIndex;
- private boolean mDownDelivered;
- private Key[] mAsciiKeys = new Key[256];
- private boolean mPlaying;
- private int mLastX;
- private int mLastY;
- private Paint mPaint;
-
- private void setKeyboardLocal(Keyboard k) {
- if (DEBUG_AUTO_PLAY) {
- findKeys();
- if (mHandler2 == null) {
- mHandler2 = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- removeMessages(MSG_TOUCH_DOWN);
- removeMessages(MSG_TOUCH_UP);
- if (mPlaying == false) return;
-
- switch (msg.what) {
- case MSG_TOUCH_DOWN:
- if (mStringIndex >= mStringToPlay.length()) {
- mPlaying = false;
- return;
- }
- char c = mStringToPlay.charAt(mStringIndex);
- while (c > 255 || mAsciiKeys[c] == null) {
- mStringIndex++;
- if (mStringIndex >= mStringToPlay.length()) {
- mPlaying = false;
- return;
- }
- c = mStringToPlay.charAt(mStringIndex);
- }
- int x = mAsciiKeys[c].x + 10;
- int y = mAsciiKeys[c].y + 26;
- MotionEvent me = MotionEvent.obtain(SystemClock.uptimeMillis(),
- SystemClock.uptimeMillis(),
- MotionEvent.ACTION_DOWN, x, y, 0);
- LatinKeyboardView.this.dispatchTouchEvent(me);
- me.recycle();
- sendEmptyMessageDelayed(MSG_TOUCH_UP, 500); // Deliver up in 500ms if nothing else
- // happens
- mDownDelivered = true;
- break;
- case MSG_TOUCH_UP:
- char cUp = mStringToPlay.charAt(mStringIndex);
- int x2 = mAsciiKeys[cUp].x + 10;
- int y2 = mAsciiKeys[cUp].y + 26;
- mStringIndex++;
-
- MotionEvent me2 = MotionEvent.obtain(SystemClock.uptimeMillis(),
- SystemClock.uptimeMillis(),
- MotionEvent.ACTION_UP, x2, y2, 0);
- LatinKeyboardView.this.dispatchTouchEvent(me2);
- me2.recycle();
- sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 500); // Deliver up in 500ms if nothing else
- // happens
- mDownDelivered = false;
- break;
- }
- }
- };
-
- }
- }
- }
-
- private void findKeys() {
- List<Key> keys = getKeyboard().getKeys();
- // Get the keys on this keyboard
- for (int i = 0; i < keys.size(); i++) {
- int code = keys.get(i).codes[0];
- if (code >= 0 && code <= 255) {
- mAsciiKeys[code] = keys.get(i);
- }
- }
- }
-
- public void startPlaying(String s) {
- if (DEBUG_AUTO_PLAY) {
- if (s == null) return;
- mStringToPlay = s.toLowerCase();
- mPlaying = true;
- mDownDelivered = false;
- mStringIndex = 0;
- mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10);
- }
- }
-
- @Override
- public void draw(Canvas c) {
- LatinIMEUtil.GCUtils.getInstance().reset();
- boolean tryGC = true;
- for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
- try {
- super.draw(c);
- tryGC = false;
- } catch (OutOfMemoryError e) {
- tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e);
- }
- }
- if (DEBUG_AUTO_PLAY) {
- if (mPlaying) {
- mHandler2.removeMessages(MSG_TOUCH_DOWN);
- mHandler2.removeMessages(MSG_TOUCH_UP);
- if (mDownDelivered) {
- mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20);
- } else {
- mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20);
- }
- }
- }
- if (DEBUG_LINE) {
- if (mPaint == null) {
- mPaint = new Paint();
- mPaint.setColor(0x80FFFFFF);
- mPaint.setAntiAlias(false);
- }
- c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint);
- c.drawLine(0, mLastY, getWidth(), mLastY, mPaint);
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/PointerTracker.java b/java/src/com/android/inputmethod/latin/PointerTracker.java
deleted file mode 100644
index b23b4d7b8..000000000
--- a/java/src/com/android/inputmethod/latin/PointerTracker.java
+++ /dev/null
@@ -1,581 +0,0 @@
-/*
- * Copyright (C) 2010 Google Inc.
- *
- * 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.latin;
-
-import com.android.inputmethod.latin.LatinKeyboardBaseView.OnKeyboardActionListener;
-import com.android.inputmethod.latin.LatinKeyboardBaseView.UIHandler;
-
-import android.content.res.Resources;
-import android.inputmethodservice.Keyboard;
-import android.inputmethodservice.Keyboard.Key;
-import android.util.Log;
-import android.view.MotionEvent;
-
-public class PointerTracker {
- private static final String TAG = "PointerTracker";
- private static final boolean DEBUG = false;
- private static final boolean DEBUG_MOVE = false;
-
- public interface UIProxy {
- public void invalidateKey(Key key);
- public void showPreview(int keyIndex, PointerTracker tracker);
- public boolean hasDistinctMultitouch();
- }
-
- public final int mPointerId;
-
- // Timing constants
- private final int mDelayBeforeKeyRepeatStart;
- private final int mLongPressKeyTimeout;
- private final int mMultiTapKeyTimeout;
-
- // Miscellaneous constants
- private static final int NOT_A_KEY = LatinKeyboardBaseView.NOT_A_KEY;
- private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
-
- private final UIProxy mProxy;
- private final UIHandler mHandler;
- private final KeyDetector mKeyDetector;
- private OnKeyboardActionListener mListener;
- private final KeyboardSwitcher mKeyboardSwitcher;
- private final boolean mHasDistinctMultitouch;
-
- private Key[] mKeys;
- private int mKeyHysteresisDistanceSquared = -1;
-
- private final KeyState mKeyState;
-
- // true if keyboard layout has been changed.
- private boolean mKeyboardLayoutHasBeenChanged;
-
- // true if event is already translated to a key action (long press or mini-keyboard)
- private boolean mKeyAlreadyProcessed;
-
- // true if this pointer is repeatable key
- private boolean mIsRepeatableKey;
-
- // true if this pointer is in sliding key input
- private boolean mIsInSlidingKeyInput;
-
- // For multi-tap
- private int mLastSentIndex;
- private int mTapCount;
- private long mLastTapTime;
- private boolean mInMultiTap;
- private final StringBuilder mPreviewLabel = new StringBuilder(1);
-
- // pressed key
- private int mPreviousKey = NOT_A_KEY;
-
- // This class keeps track of a key index and a position where this pointer is.
- private static class KeyState {
- private final KeyDetector mKeyDetector;
-
- // The position and time at which first down event occurred.
- private int mStartX;
- private int mStartY;
- private long mDownTime;
-
- // The current key index where this pointer is.
- private int mKeyIndex = NOT_A_KEY;
- // The position where mKeyIndex was recognized for the first time.
- private int mKeyX;
- private int mKeyY;
-
- // Last pointer position.
- private int mLastX;
- private int mLastY;
-
- public KeyState(KeyDetector keyDetecor) {
- mKeyDetector = keyDetecor;
- }
-
- public int getKeyIndex() {
- return mKeyIndex;
- }
-
- public int getKeyX() {
- return mKeyX;
- }
-
- public int getKeyY() {
- return mKeyY;
- }
-
- public int getStartX() {
- return mStartX;
- }
-
- public int getStartY() {
- return mStartY;
- }
-
- public long getDownTime() {
- return mDownTime;
- }
-
- public int getLastX() {
- return mLastX;
- }
-
- public int getLastY() {
- return mLastY;
- }
-
- public int onDownKey(int x, int y, long eventTime) {
- mStartX = x;
- mStartY = y;
- mDownTime = eventTime;
-
- return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
- }
-
- private int onMoveKeyInternal(int x, int y) {
- mLastX = x;
- mLastY = y;
- return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
- }
-
- public int onMoveKey(int x, int y) {
- return onMoveKeyInternal(x, y);
- }
-
- public int onMoveToNewKey(int keyIndex, int x, int y) {
- mKeyIndex = keyIndex;
- mKeyX = x;
- mKeyY = y;
- return keyIndex;
- }
-
- public int onUpKey(int x, int y) {
- return onMoveKeyInternal(x, y);
- }
- }
-
- public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy,
- Resources res) {
- if (proxy == null || handler == null || keyDetector == null)
- throw new NullPointerException();
- mPointerId = id;
- mProxy = proxy;
- mHandler = handler;
- mKeyDetector = keyDetector;
- mKeyboardSwitcher = KeyboardSwitcher.getInstance();
- mKeyState = new KeyState(keyDetector);
- mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
- mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
- mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
- mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout);
- resetMultiTap();
- }
-
- public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
- mListener = listener;
- }
-
- public void setKeyboard(Key[] keys, float keyHysteresisDistance) {
- if (keys == null || keyHysteresisDistance < 0)
- throw new IllegalArgumentException();
- mKeys = keys;
- mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
- // Mark that keyboard layout has been changed.
- mKeyboardLayoutHasBeenChanged = true;
- }
-
- public boolean isInSlidingKeyInput() {
- return mIsInSlidingKeyInput;
- }
-
- private boolean isValidKeyIndex(int keyIndex) {
- return keyIndex >= 0 && keyIndex < mKeys.length;
- }
-
- public Key getKey(int keyIndex) {
- return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null;
- }
-
- private boolean isModifierInternal(int keyIndex) {
- Key key = getKey(keyIndex);
- if (key == null)
- return false;
- int primaryCode = key.codes[0];
- return primaryCode == Keyboard.KEYCODE_SHIFT
- || primaryCode == Keyboard.KEYCODE_MODE_CHANGE;
- }
-
- public boolean isModifier() {
- return isModifierInternal(mKeyState.getKeyIndex());
- }
-
- public boolean isOnModifierKey(int x, int y) {
- return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
- }
-
- public boolean isSpaceKey(int keyIndex) {
- Key key = getKey(keyIndex);
- return key != null && key.codes[0] == LatinIME.KEYCODE_SPACE;
- }
-
- public void updateKey(int keyIndex) {
- if (mKeyAlreadyProcessed)
- return;
- int oldKeyIndex = mPreviousKey;
- mPreviousKey = keyIndex;
- if (keyIndex != oldKeyIndex) {
- if (isValidKeyIndex(oldKeyIndex)) {
- // if new key index is not a key, old key was just released inside of the key.
- final boolean inside = (keyIndex == NOT_A_KEY);
- mKeys[oldKeyIndex].onReleased(inside);
- mProxy.invalidateKey(mKeys[oldKeyIndex]);
- }
- if (isValidKeyIndex(keyIndex)) {
- mKeys[keyIndex].onPressed();
- mProxy.invalidateKey(mKeys[keyIndex]);
- }
- }
- }
-
- public void setAlreadyProcessed() {
- mKeyAlreadyProcessed = true;
- }
-
- public void onTouchEvent(int action, int x, int y, long eventTime) {
- switch (action) {
- case MotionEvent.ACTION_MOVE:
- onMoveEvent(x, y, eventTime);
- break;
- case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_POINTER_DOWN:
- onDownEvent(x, y, eventTime);
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_POINTER_UP:
- onUpEvent(x, y, eventTime);
- break;
- case MotionEvent.ACTION_CANCEL:
- onCancelEvent(x, y, eventTime);
- break;
- }
- }
-
- public void onDownEvent(int x, int y, long eventTime) {
- if (DEBUG)
- debugLog("onDownEvent:", x, y);
- int keyIndex = mKeyState.onDownKey(x, y, eventTime);
- mKeyboardLayoutHasBeenChanged = false;
- mKeyAlreadyProcessed = false;
- mIsRepeatableKey = false;
- mIsInSlidingKeyInput = false;
- checkMultiTap(eventTime, keyIndex);
- if (mListener != null) {
- if (isValidKeyIndex(keyIndex)) {
- mListener.onPress(mKeys[keyIndex].codes[0]);
- // This onPress call may have changed keyboard layout. Those cases are detected at
- // {@link #setKeyboard}. In those cases, we should update keyIndex according to the
- // new keyboard layout.
- if (mKeyboardLayoutHasBeenChanged) {
- mKeyboardLayoutHasBeenChanged = false;
- keyIndex = mKeyState.onDownKey(x, y, eventTime);
- }
- }
- }
- if (isValidKeyIndex(keyIndex)) {
- if (mKeys[keyIndex].repeatable) {
- repeatKey(keyIndex);
- mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
- mIsRepeatableKey = true;
- }
- startLongPressTimer(keyIndex);
- }
- showKeyPreviewAndUpdateKey(keyIndex);
- }
-
- public void onMoveEvent(int x, int y, long eventTime) {
- if (DEBUG_MOVE)
- debugLog("onMoveEvent:", x, y);
- if (mKeyAlreadyProcessed)
- return;
- final KeyState keyState = mKeyState;
- int keyIndex = keyState.onMoveKey(x, y);
- final Key oldKey = getKey(keyState.getKeyIndex());
- if (isValidKeyIndex(keyIndex)) {
- if (oldKey == null) {
- // The pointer has been slid in to the new key, but the finger was not on any keys.
- // In this case, we must call onPress() to notify that the new key is being pressed.
- if (mListener != null) {
- mListener.onPress(getKey(keyIndex).codes[0]);
- // This onPress call may have changed keyboard layout. Those cases are detected
- // at {@link #setKeyboard}. In those cases, we should update keyIndex according
- // to the new keyboard layout.
- if (mKeyboardLayoutHasBeenChanged) {
- mKeyboardLayoutHasBeenChanged = false;
- keyIndex = keyState.onMoveKey(x, y);
- }
- }
- keyState.onMoveToNewKey(keyIndex, x, y);
- startLongPressTimer(keyIndex);
- } else if (!isMinorMoveBounce(x, y, keyIndex)) {
- // The pointer has been slid in to the new key from the previous key, we must call
- // onRelease() first to notify that the previous key has been released, then call
- // onPress() to notify that the new key is being pressed.
- mIsInSlidingKeyInput = true;
- if (mListener != null)
- mListener.onRelease(oldKey.codes[0]);
- resetMultiTap();
- if (mListener != null) {
- mListener.onPress(getKey(keyIndex).codes[0]);
- // This onPress call may have changed keyboard layout. Those cases are detected
- // at {@link #setKeyboard}. In those cases, we should update keyIndex according
- // to the new keyboard layout.
- if (mKeyboardLayoutHasBeenChanged) {
- mKeyboardLayoutHasBeenChanged = false;
- keyIndex = keyState.onMoveKey(x, y);
- }
- }
- keyState.onMoveToNewKey(keyIndex, x, y);
- startLongPressTimer(keyIndex);
- }
- } else {
- if (oldKey != null && !isMinorMoveBounce(x, y, keyIndex)) {
- // The pointer has been slid out from the previous key, we must call onRelease() to
- // notify that the previous key has been released.
- mIsInSlidingKeyInput = true;
- if (mListener != null)
- mListener.onRelease(oldKey.codes[0]);
- resetMultiTap();
- keyState.onMoveToNewKey(keyIndex, x ,y);
- mHandler.cancelLongPressTimer();
- }
- }
- showKeyPreviewAndUpdateKey(keyState.getKeyIndex());
- }
-
- public void onUpEvent(int x, int y, long eventTime) {
- if (DEBUG)
- debugLog("onUpEvent :", x, y);
- mHandler.cancelKeyTimers();
- mHandler.cancelPopupPreview();
- showKeyPreviewAndUpdateKey(NOT_A_KEY);
- mIsInSlidingKeyInput = false;
- if (mKeyAlreadyProcessed)
- return;
- int keyIndex = mKeyState.onUpKey(x, y);
- if (isMinorMoveBounce(x, y, keyIndex)) {
- // Use previous fixed key index and coordinates.
- keyIndex = mKeyState.getKeyIndex();
- x = mKeyState.getKeyX();
- y = mKeyState.getKeyY();
- }
- if (!mIsRepeatableKey) {
- detectAndSendKey(keyIndex, x, y, eventTime);
- }
-
- if (isValidKeyIndex(keyIndex))
- mProxy.invalidateKey(mKeys[keyIndex]);
- }
-
- public void onCancelEvent(int x, int y, long eventTime) {
- if (DEBUG)
- debugLog("onCancelEvt:", x, y);
- mHandler.cancelKeyTimers();
- mHandler.cancelPopupPreview();
- showKeyPreviewAndUpdateKey(NOT_A_KEY);
- mIsInSlidingKeyInput = false;
- int keyIndex = mKeyState.getKeyIndex();
- if (isValidKeyIndex(keyIndex))
- mProxy.invalidateKey(mKeys[keyIndex]);
- }
-
- public void repeatKey(int keyIndex) {
- Key key = getKey(keyIndex);
- if (key != null) {
- // While key is repeating, because there is no need to handle multi-tap key, we can
- // pass -1 as eventTime argument.
- detectAndSendKey(keyIndex, key.x, key.y, -1);
- }
- }
-
- public int getLastX() {
- return mKeyState.getLastX();
- }
-
- public int getLastY() {
- return mKeyState.getLastY();
- }
-
- public long getDownTime() {
- return mKeyState.getDownTime();
- }
-
- // These package scope methods are only for debugging purpose.
- /* package */ int getStartX() {
- return mKeyState.getStartX();
- }
-
- /* package */ int getStartY() {
- return mKeyState.getStartY();
- }
-
- private boolean isMinorMoveBounce(int x, int y, int newKey) {
- if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
- throw new IllegalStateException("keyboard and/or hysteresis not set");
- int curKey = mKeyState.getKeyIndex();
- if (newKey == curKey) {
- return true;
- } else if (isValidKeyIndex(curKey)) {
- return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) < mKeyHysteresisDistanceSquared;
- } else {
- return false;
- }
- }
-
- private static int getSquareDistanceToKeyEdge(int x, int y, Key key) {
- final int left = key.x;
- final int right = key.x + key.width;
- final int top = key.y;
- final int bottom = key.y + key.height;
- final int edgeX = x < left ? left : (x > right ? right : x);
- final int edgeY = y < top ? top : (y > bottom ? bottom : y);
- final int dx = x - edgeX;
- final int dy = y - edgeY;
- return dx * dx + dy * dy;
- }
-
- private void showKeyPreviewAndUpdateKey(int keyIndex) {
- updateKey(keyIndex);
- // The modifier key, such as shift key, should not be shown as preview when multi-touch is
- // supported. On the other hand, if multi-touch is not supported, the modifier key should
- // be shown as preview.
- if (mHasDistinctMultitouch && isModifier()) {
- mProxy.showPreview(NOT_A_KEY, this);
- } else {
- mProxy.showPreview(keyIndex, this);
- }
- }
-
- private void startLongPressTimer(int keyIndex) {
- if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) {
- // We use longer timeout for sliding finger input started from the symbols mode key.
- mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this);
- } else {
- mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
- }
- }
-
- private void detectAndSendKey(int index, int x, int y, long eventTime) {
- final OnKeyboardActionListener listener = mListener;
- final Key key = getKey(index);
-
- if (key == null) {
- if (listener != null)
- listener.onCancel();
- } else {
- if (key.text != null) {
- if (listener != null) {
- listener.onText(key.text);
- listener.onRelease(0); // dummy key code
- }
- } else {
- int code = key.codes[0];
- int[] codes = mKeyDetector.newCodeArray();
- mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
- // Multi-tap
- if (mInMultiTap) {
- if (mTapCount != -1) {
- mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y);
- } else {
- mTapCount = 0;
- }
- code = key.codes[mTapCount];
- }
- /*
- * Swap the first and second values in the codes array if the primary code is not
- * the first value but the second value in the array. This happens when key
- * debouncing is in effect.
- */
- if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
- codes[1] = codes[0];
- codes[0] = code;
- }
- if (listener != null) {
- listener.onKey(code, codes, x, y);
- listener.onRelease(code);
- }
- }
- mLastSentIndex = index;
- mLastTapTime = eventTime;
- }
- }
-
- /**
- * Handle multi-tap keys by producing the key label for the current multi-tap state.
- */
- public CharSequence getPreviewText(Key key) {
- if (mInMultiTap) {
- // Multi-tap
- mPreviewLabel.setLength(0);
- mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
- return mPreviewLabel;
- } else {
- return key.label;
- }
- }
-
- private void resetMultiTap() {
- mLastSentIndex = NOT_A_KEY;
- mTapCount = 0;
- mLastTapTime = -1;
- mInMultiTap = false;
- }
-
- private void checkMultiTap(long eventTime, int keyIndex) {
- Key key = getKey(keyIndex);
- if (key == null)
- return;
-
- final boolean isMultiTap =
- (eventTime < mLastTapTime + mMultiTapKeyTimeout && keyIndex == mLastSentIndex);
- if (key.codes.length > 1) {
- mInMultiTap = true;
- if (isMultiTap) {
- mTapCount = (mTapCount + 1) % key.codes.length;
- return;
- } else {
- mTapCount = -1;
- return;
- }
- }
- if (!isMultiTap) {
- resetMultiTap();
- }
- }
-
- private void debugLog(String title, int x, int y) {
- int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
- Key key = getKey(keyIndex);
- final String code;
- if (key == null) {
- code = "----";
- } else {
- int primaryCode = key.codes[0];
- code = String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode);
- }
- Log.d(TAG, String.format("%s%s[%d] %3d,%3d %3d(%s) %s", title,
- (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, keyIndex, code,
- (isModifier() ? "modifier" : "")));
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/ModifierKeyState.java b/java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java
index 097e87abe..eb740e111 100644
--- a/java/src/com/android/inputmethod/latin/ModifierKeyState.java
+++ b/java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 Google Inc.
+ * 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
@@ -16,27 +16,14 @@
package com.android.inputmethod.latin;
-class ModifierKeyState {
- private static final int RELEASING = 0;
- private static final int PRESSING = 1;
- private static final int MOMENTARY = 2;
+import android.content.Context;
- private int mState = RELEASING;
+import java.util.List;
+import java.util.Locale;
- public void onPress() {
- mState = PRESSING;
- }
-
- public void onRelease() {
- mState = RELEASING;
- }
-
- public void onOtherKeyPressed() {
- if (mState == PRESSING)
- mState = MOMENTARY;
- }
-
- public boolean isMomentary() {
- return mState == MOMENTARY;
+class PrivateBinaryDictionaryGetter {
+ private PrivateBinaryDictionaryGetter() {}
+ public static List<AssetFileAddress> getDictionaryFiles(Locale locale, Context context) {
+ return null;
}
}
diff --git a/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java b/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java
deleted file mode 100644
index 325ce674c..000000000
--- a/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2010 Google Inc.
- *
- * 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.latin;
-
-import android.inputmethodservice.Keyboard.Key;
-
-import java.util.Arrays;
-
-class ProximityKeyDetector extends KeyDetector {
- private static final int MAX_NEARBY_KEYS = 12;
-
- // working area
- private int[] mDistances = new int[MAX_NEARBY_KEYS];
-
- @Override
- protected int getMaxNearbyKeys() {
- return MAX_NEARBY_KEYS;
- }
-
- @Override
- public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) {
- final Key[] keys = getKeys();
- final int touchX = getTouchX(x);
- final int touchY = getTouchY(y);
- int primaryIndex = LatinKeyboardBaseView.NOT_A_KEY;
- int closestKey = LatinKeyboardBaseView.NOT_A_KEY;
- int closestKeyDist = mProximityThresholdSquare + 1;
- int[] distances = mDistances;
- Arrays.fill(distances, Integer.MAX_VALUE);
- int [] nearestKeyIndices = mKeyboard.getNearestKeys(touchX, touchY);
- final int keyCount = nearestKeyIndices.length;
- for (int i = 0; i < keyCount; i++) {
- final Key key = keys[nearestKeyIndices[i]];
- int dist = 0;
- boolean isInside = key.isInside(touchX, touchY);
- if (isInside) {
- primaryIndex = nearestKeyIndices[i];
- }
-
- if (((mProximityCorrectOn
- && (dist = key.squaredDistanceFrom(touchX, touchY)) < mProximityThresholdSquare)
- || isInside)
- && key.codes[0] > 32) {
- // Find insertion point
- final int nCodes = key.codes.length;
- if (dist < closestKeyDist) {
- closestKeyDist = dist;
- closestKey = nearestKeyIndices[i];
- }
-
- if (allKeys == null) continue;
-
- for (int j = 0; j < distances.length; j++) {
- if (distances[j] > dist) {
- // Make space for nCodes codes
- System.arraycopy(distances, j, distances, j + nCodes,
- distances.length - j - nCodes);
- System.arraycopy(allKeys, j, allKeys, j + nCodes,
- allKeys.length - j - nCodes);
- System.arraycopy(key.codes, 0, allKeys, j, nCodes);
- Arrays.fill(distances, j, j + nCodes, dist);
- break;
- }
- }
- }
- }
- if (primaryIndex == LatinKeyboardBaseView.NOT_A_KEY) {
- primaryIndex = closestKey;
- }
- return primaryIndex;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
new file mode 100644
index 000000000..6c515c845
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -0,0 +1,603 @@
+/*
+ * 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.latin;
+
+import com.android.inputmethod.compat.CompatUtils;
+import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
+import com.android.inputmethod.deprecated.VoiceProxy;
+import com.android.inputmethod.compat.VibratorCompatWrapper;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.backup.BackupManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceScreen;
+import android.speech.SpeechRecognizer;
+import android.text.AutoText;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.util.Log;
+import android.widget.TextView;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+public class Settings extends PreferenceActivity
+ implements SharedPreferences.OnSharedPreferenceChangeListener,
+ DialogInterface.OnDismissListener, OnPreferenceClickListener {
+ private static final String TAG = "Settings";
+
+ public static final String PREF_GENERAL_SETTINGS_KEY = "general_settings";
+ public static final String PREF_VIBRATE_ON = "vibrate_on";
+ public static final String PREF_SOUND_ON = "sound_on";
+ public static final String PREF_KEY_PREVIEW_POPUP_ON = "popup_on";
+ public static final String PREF_RECORRECTION_ENABLED = "recorrection_enabled";
+ public static final String PREF_AUTO_CAP = "auto_cap";
+ public static final String PREF_SETTINGS_KEY = "settings_key";
+ public static final String PREF_VOICE_SETTINGS_KEY = "voice_mode";
+ public static final String PREF_INPUT_LANGUAGE = "input_language";
+ public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
+ public static final String PREF_SUBTYPES = "subtype_settings";
+
+ public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key";
+ public static final String PREF_CORRECTION_SETTINGS_KEY = "correction_settings";
+ public static final String PREF_QUICK_FIXES = "quick_fixes";
+ public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting";
+ public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold";
+ public static final String PREF_DEBUG_SETTINGS = "debug_settings";
+
+ public static final String PREF_NGRAM_SETTINGS_KEY = "ngram_settings";
+ public static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion";
+ public static final String PREF_BIGRAM_PREDICTIONS = "bigram_prediction";
+
+ public static final String PREF_MISC_SETTINGS_KEY = "misc_settings";
+
+ public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
+ "pref_key_preview_popup_dismiss_delay";
+ public static final String PREF_KEY_USE_CONTACTS_DICT =
+ "pref_key_use_contacts_dict";
+
+ public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
+
+ // Dialog ids
+ private static final int VOICE_INPUT_CONFIRM_DIALOG = 0;
+
+ public static class Values {
+ // From resources:
+ public final boolean mSwipeDownDismissKeyboardEnabled;
+ public final int mDelayBeforeFadeoutLanguageOnSpacebar;
+ public final int mDelayUpdateSuggestions;
+ public final int mDelayUpdateOldSuggestions;
+ public final int mDelayUpdateShiftState;
+ public final int mDurationOfFadeoutLanguageOnSpacebar;
+ public final float mFinalFadeoutFactorOfLanguageOnSpacebar;
+ public final long mDoubleSpacesTurnIntoPeriodTimeout;
+ public final String mWordSeparators;
+ public final String mMagicSpaceStrippers;
+ public final String mMagicSpaceSwappers;
+ public final String mSuggestPuncs;
+ public final SuggestedWords mSuggestPuncList;
+
+ // From preferences:
+ public final boolean mSoundOn; // Sound setting private to Latin IME (see mSilentModeOn)
+ public final boolean mVibrateOn;
+ public final boolean mKeyPreviewPopupOn;
+ public final int mKeyPreviewPopupDismissDelay;
+ public final boolean mAutoCap;
+ public final boolean mQuickFixes;
+ public final boolean mAutoCorrectEnabled;
+ public final double mAutoCorrectionThreshold;
+ // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
+ public final boolean mBigramSuggestionEnabled;
+ // Prediction: use bigrams to predict the next word when there is no input for it yet
+ public final boolean mBigramPredictionEnabled;
+ public final boolean mUseContactsDict;
+
+ public Values(final SharedPreferences prefs, final Context context,
+ final String localeStr) {
+ final Resources res = context.getResources();
+ final Locale savedLocale;
+ if (null != localeStr) {
+ final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr);
+ savedLocale = Utils.setSystemLocale(res, keyboardLocale);
+ } else {
+ savedLocale = null;
+ }
+
+ // Get the resources
+ mSwipeDownDismissKeyboardEnabled = res.getBoolean(
+ R.bool.config_swipe_down_dismiss_keyboard_enabled);
+ mDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger(
+ R.integer.config_delay_before_fadeout_language_on_spacebar);
+ mDelayUpdateSuggestions =
+ res.getInteger(R.integer.config_delay_update_suggestions);
+ mDelayUpdateOldSuggestions = res.getInteger(
+ R.integer.config_delay_update_old_suggestions);
+ mDelayUpdateShiftState =
+ res.getInteger(R.integer.config_delay_update_shift_state);
+ mDurationOfFadeoutLanguageOnSpacebar = res.getInteger(
+ R.integer.config_duration_of_fadeout_language_on_spacebar);
+ mFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger(
+ R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f;
+ mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger(
+ R.integer.config_double_spaces_turn_into_period_timeout);
+ mMagicSpaceStrippers = res.getString(R.string.magic_space_stripping_symbols);
+ mMagicSpaceSwappers = res.getString(R.string.magic_space_swapping_symbols);
+ String wordSeparators = mMagicSpaceStrippers + mMagicSpaceSwappers
+ + res.getString(R.string.magic_space_promoting_symbols);
+ final String notWordSeparators = res.getString(R.string.non_word_separator_symbols);
+ for (int i = notWordSeparators.length() - 1; i >= 0; --i) {
+ wordSeparators = wordSeparators.replace(notWordSeparators.substring(i, i + 1), "");
+ }
+ mWordSeparators = wordSeparators;
+ mSuggestPuncs = res.getString(R.string.suggested_punctuations);
+ // TODO: it would be nice not to recreate this each time we change the configuration
+ mSuggestPuncList = createSuggestPuncList(mSuggestPuncs);
+
+ // Get the settings preferences
+ final boolean hasVibrator = VibratorCompatWrapper.getInstance(context).hasVibrator();
+ mVibrateOn = hasVibrator && prefs.getBoolean(Settings.PREF_VIBRATE_ON, false);
+ mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON,
+ res.getBoolean(R.bool.config_default_sound_enabled));
+
+ mKeyPreviewPopupOn = isKeyPreviewPopupEnabled(prefs, res);
+ mKeyPreviewPopupDismissDelay = getKeyPreviewPopupDismissDelay(prefs, res);
+ mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
+ mQuickFixes = isQuickFixesEnabled(prefs, res);
+
+ mAutoCorrectEnabled = isAutoCorrectEnabled(prefs, res);
+ mBigramSuggestionEnabled = mAutoCorrectEnabled
+ && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled);
+ mBigramPredictionEnabled = mBigramSuggestionEnabled
+ && isBigramPredictionEnabled(prefs, res);
+
+ mAutoCorrectionThreshold = getAutoCorrectionThreshold(prefs, res);
+
+ mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
+
+ Utils.setSystemLocale(res, savedLocale);
+ }
+
+ public boolean isSuggestedPunctuation(int code) {
+ return mSuggestPuncs.contains(String.valueOf((char)code));
+ }
+
+ public boolean isWordSeparator(int code) {
+ return mWordSeparators.contains(String.valueOf((char)code));
+ }
+
+ public boolean isMagicSpaceStripper(int code) {
+ return mMagicSpaceStrippers.contains(String.valueOf((char)code));
+ }
+
+ public boolean isMagicSpaceSwapper(int code) {
+ return mMagicSpaceSwappers.contains(String.valueOf((char)code));
+ }
+
+ // Helper methods
+ private static boolean isQuickFixesEnabled(SharedPreferences sp, Resources resources) {
+ final boolean showQuickFixesOption = resources.getBoolean(
+ R.bool.config_enable_quick_fixes_option);
+ if (!showQuickFixesOption) {
+ return isAutoCorrectEnabled(sp, resources);
+ }
+ return sp.getBoolean(Settings.PREF_QUICK_FIXES, resources.getBoolean(
+ R.bool.config_default_quick_fixes));
+ }
+
+ private static boolean isAutoCorrectEnabled(SharedPreferences sp, Resources resources) {
+ final String currentAutoCorrectionSetting = sp.getString(
+ Settings.PREF_AUTO_CORRECTION_THRESHOLD,
+ resources.getString(R.string.auto_correction_threshold_mode_index_modest));
+ final String autoCorrectionOff = resources.getString(
+ R.string.auto_correction_threshold_mode_index_off);
+ return !currentAutoCorrectionSetting.equals(autoCorrectionOff);
+ }
+
+ // Public to access from KeyboardSwitcher. Should it have access to some
+ // process-global instance instead?
+ public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) {
+ final boolean showPopupOption = resources.getBoolean(
+ R.bool.config_enable_show_popup_on_keypress_option);
+ if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview);
+ return sp.getBoolean(Settings.PREF_KEY_PREVIEW_POPUP_ON,
+ resources.getBoolean(R.bool.config_default_popup_preview));
+ }
+
+ // Likewise
+ public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp,
+ Resources resources) {
+ return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+ Integer.toString(resources.getInteger(R.integer.config_delay_after_preview))));
+ }
+
+ private static boolean isBigramSuggestionEnabled(SharedPreferences sp, Resources resources,
+ boolean autoCorrectEnabled) {
+ final boolean showBigramSuggestionsOption = resources.getBoolean(
+ R.bool.config_enable_bigram_suggestions_option);
+ if (!showBigramSuggestionsOption) {
+ return autoCorrectEnabled;
+ }
+ return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, resources.getBoolean(
+ R.bool.config_default_bigram_suggestions));
+ }
+
+ private static boolean isBigramPredictionEnabled(SharedPreferences sp,
+ Resources resources) {
+ return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean(
+ R.bool.config_default_bigram_prediction));
+ }
+
+ private static double getAutoCorrectionThreshold(SharedPreferences sp,
+ Resources resources) {
+ final String currentAutoCorrectionSetting = sp.getString(
+ Settings.PREF_AUTO_CORRECTION_THRESHOLD,
+ resources.getString(R.string.auto_correction_threshold_mode_index_modest));
+ final String[] autoCorrectionThresholdValues = resources.getStringArray(
+ R.array.auto_correction_threshold_values);
+ // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off.
+ double autoCorrectionThreshold = Double.MAX_VALUE;
+ try {
+ final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting);
+ if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) {
+ autoCorrectionThreshold = Double.parseDouble(
+ autoCorrectionThresholdValues[arrayIndex]);
+ }
+ } catch (NumberFormatException e) {
+ // Whenever the threshold settings are correct, never come here.
+ autoCorrectionThreshold = Double.MAX_VALUE;
+ Log.w(TAG, "Cannot load auto correction threshold setting."
+ + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting
+ + ", autoCorrectionThresholdValues: "
+ + Arrays.toString(autoCorrectionThresholdValues));
+ }
+ return autoCorrectionThreshold;
+ }
+
+ private static SuggestedWords createSuggestPuncList(final String puncs) {
+ SuggestedWords.Builder builder = new SuggestedWords.Builder();
+ if (puncs != null) {
+ for (int i = 0; i < puncs.length(); i++) {
+ builder.addWord(puncs.subSequence(i, i + 1));
+ }
+ }
+ return builder.build();
+ }
+ }
+
+ private PreferenceScreen mInputLanguageSelection;
+ private CheckBoxPreference mQuickFixes;
+ private ListPreference mVoicePreference;
+ private ListPreference mSettingsKeyPreference;
+ private ListPreference mShowCorrectionSuggestionsPreference;
+ private ListPreference mAutoCorrectionThreshold;
+ private ListPreference mKeyPreviewPopupDismissDelay;
+ // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
+ private CheckBoxPreference mBigramSuggestion;
+ // Prediction: use bigrams to predict the next word when there is no input for it yet
+ private CheckBoxPreference mBigramPrediction;
+ private Preference mDebugSettingsPreference;
+ private boolean mVoiceOn;
+
+ private AlertDialog mDialog;
+
+ private VoiceProxy.VoiceLoggerWrapper mVoiceLogger;
+
+ private boolean mOkClicked = false;
+ private String mVoiceModeOff;
+
+ private void ensureConsistencyOfAutoCorrectionSettings() {
+ final String autoCorrectionOff = getResources().getString(
+ R.string.auto_correction_threshold_mode_index_off);
+ final String currentSetting = mAutoCorrectionThreshold.getValue();
+ mBigramSuggestion.setEnabled(!currentSetting.equals(autoCorrectionOff));
+ mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ final Resources res = getResources();
+
+ addPreferencesFromResource(R.xml.prefs);
+ mInputLanguageSelection = (PreferenceScreen) findPreference(PREF_SUBTYPES);
+ mInputLanguageSelection.setOnPreferenceClickListener(this);
+ mQuickFixes = (CheckBoxPreference) findPreference(PREF_QUICK_FIXES);
+ mVoicePreference = (ListPreference) findPreference(PREF_VOICE_SETTINGS_KEY);
+ mSettingsKeyPreference = (ListPreference) findPreference(PREF_SETTINGS_KEY);
+ mShowCorrectionSuggestionsPreference =
+ (ListPreference) findPreference(PREF_SHOW_SUGGESTIONS_SETTING);
+ SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ mVoiceModeOff = getString(R.string.voice_mode_off);
+ mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff)
+ .equals(mVoiceModeOff));
+ mVoiceLogger = VoiceProxy.VoiceLoggerWrapper.getInstance(this);
+
+ mAutoCorrectionThreshold = (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
+ mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTIONS);
+ mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS);
+ mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
+ if (mDebugSettingsPreference != null) {
+ final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
+ debugSettingsIntent.setClassName(getPackageName(), DebugSettings.class.getName());
+ mDebugSettingsPreference.setIntent(debugSettingsIntent);
+ }
+
+ ensureConsistencyOfAutoCorrectionSettings();
+
+ final PreferenceGroup generalSettings =
+ (PreferenceGroup) findPreference(PREF_GENERAL_SETTINGS_KEY);
+ final PreferenceGroup textCorrectionGroup =
+ (PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY);
+
+ final boolean showSettingsKeyOption = res.getBoolean(
+ R.bool.config_enable_show_settings_key_option);
+ if (!showSettingsKeyOption) {
+ generalSettings.removePreference(mSettingsKeyPreference);
+ }
+
+ final boolean showVoiceKeyOption = res.getBoolean(
+ R.bool.config_enable_show_voice_key_option);
+ if (!showVoiceKeyOption) {
+ generalSettings.removePreference(mVoicePreference);
+ }
+
+ if (!VibratorCompatWrapper.getInstance(this).hasVibrator()) {
+ generalSettings.removePreference(findPreference(PREF_VIBRATE_ON));
+ }
+
+ if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
+ generalSettings.removePreference(findPreference(PREF_SUBTYPES));
+ }
+
+ final boolean showPopupOption = res.getBoolean(
+ R.bool.config_enable_show_popup_on_keypress_option);
+ if (!showPopupOption) {
+ generalSettings.removePreference(findPreference(PREF_KEY_PREVIEW_POPUP_ON));
+ }
+
+ final boolean showRecorrectionOption = res.getBoolean(
+ R.bool.config_enable_show_recorrection_option);
+ if (!showRecorrectionOption) {
+ generalSettings.removePreference(findPreference(PREF_RECORRECTION_ENABLED));
+ }
+
+ final boolean showQuickFixesOption = res.getBoolean(
+ R.bool.config_enable_quick_fixes_option);
+ if (!showQuickFixesOption) {
+ textCorrectionGroup.removePreference(findPreference(PREF_QUICK_FIXES));
+ }
+
+ final boolean showBigramSuggestionsOption = res.getBoolean(
+ R.bool.config_enable_bigram_suggestions_option);
+ if (!showBigramSuggestionsOption) {
+ textCorrectionGroup.removePreference(findPreference(PREF_BIGRAM_SUGGESTIONS));
+ textCorrectionGroup.removePreference(findPreference(PREF_BIGRAM_PREDICTIONS));
+ }
+
+ final boolean showUsabilityModeStudyOption = res.getBoolean(
+ R.bool.config_enable_usability_study_mode_option);
+ if (!showUsabilityModeStudyOption) {
+ getPreferenceScreen().removePreference(findPreference(PREF_USABILITY_STUDY_MODE));
+ }
+
+ mKeyPreviewPopupDismissDelay =
+ (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ final String[] entries = new String[] {
+ res.getString(R.string.key_preview_popup_dismiss_no_delay),
+ res.getString(R.string.key_preview_popup_dismiss_default_delay),
+ };
+ final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
+ R.integer.config_delay_after_preview));
+ mKeyPreviewPopupDismissDelay.setEntries(entries);
+ mKeyPreviewPopupDismissDelay.setEntryValues(
+ new String[] { "0", popupDismissDelayDefaultValue });
+ if (null == mKeyPreviewPopupDismissDelay.getValue()) {
+ mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
+ }
+ mKeyPreviewPopupDismissDelay.setEnabled(
+ Settings.Values.isKeyPreviewPopupEnabled(prefs, res));
+
+ final PreferenceScreen dictionaryLink =
+ (PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY);
+ final Intent intent = dictionaryLink.getIntent();
+
+ final int number = getPackageManager().queryIntentActivities(intent, 0).size();
+ if (0 >= number) {
+ textCorrectionGroup.removePreference(dictionaryLink);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ int autoTextSize = AutoText.getSize(getListView());
+ if (autoTextSize < 1) {
+ ((PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY))
+ .removePreference(mQuickFixes);
+ }
+ if (!VoiceProxy.VOICE_INSTALLED
+ || !SpeechRecognizer.isRecognitionAvailable(this)) {
+ getPreferenceScreen().removePreference(mVoicePreference);
+ } else {
+ updateVoiceModeSummary();
+ }
+ updateSettingsKeySummary();
+ updateShowCorrectionSuggestionsSummary();
+ updateKeyPreviewPopupDelaySummary();
+ }
+
+ @Override
+ protected void onDestroy() {
+ getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
+ this);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ (new BackupManager(this)).dataChanged();
+ // If turning on voice input, show dialog
+ if (key.equals(PREF_VOICE_SETTINGS_KEY) && !mVoiceOn) {
+ if (!prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff)
+ .equals(mVoiceModeOff)) {
+ showVoiceConfirmation();
+ }
+ } else if (key.equals(PREF_KEY_PREVIEW_POPUP_ON)) {
+ final ListPreference popupDismissDelay =
+ (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ if (null != popupDismissDelay) {
+ popupDismissDelay.setEnabled(prefs.getBoolean(PREF_KEY_PREVIEW_POPUP_ON, true));
+ }
+ }
+ ensureConsistencyOfAutoCorrectionSettings();
+ mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff)
+ .equals(mVoiceModeOff));
+ updateVoiceModeSummary();
+ updateSettingsKeySummary();
+ updateShowCorrectionSuggestionsSummary();
+ updateKeyPreviewPopupDelaySummary();
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference pref) {
+ if (pref == mInputLanguageSelection) {
+ startActivity(CompatUtils.getInputLanguageSelectionIntent(
+ Utils.getInputMethodId(InputMethodManagerCompatWrapper.getInstance(this),
+ getApplicationInfo().packageName), 0));
+ return true;
+ }
+ return false;
+ }
+
+ private void updateShowCorrectionSuggestionsSummary() {
+ mShowCorrectionSuggestionsPreference.setSummary(
+ getResources().getStringArray(R.array.prefs_suggestion_visibilities)
+ [mShowCorrectionSuggestionsPreference.findIndexOfValue(
+ mShowCorrectionSuggestionsPreference.getValue())]);
+ }
+
+ private void updateSettingsKeySummary() {
+ mSettingsKeyPreference.setSummary(
+ getResources().getStringArray(R.array.settings_key_modes)
+ [mSettingsKeyPreference.findIndexOfValue(mSettingsKeyPreference.getValue())]);
+ }
+
+ private void updateKeyPreviewPopupDelaySummary() {
+ final ListPreference lp = mKeyPreviewPopupDismissDelay;
+ lp.setSummary(lp.getEntries()[lp.findIndexOfValue(lp.getValue())]);
+ }
+
+ private void showVoiceConfirmation() {
+ mOkClicked = false;
+ showDialog(VOICE_INPUT_CONFIRM_DIALOG);
+ // Make URL in the dialog message clickable
+ if (mDialog != null) {
+ TextView textView = (TextView) mDialog.findViewById(android.R.id.message);
+ if (textView != null) {
+ textView.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ }
+
+ private void updateVoiceModeSummary() {
+ mVoicePreference.setSummary(
+ getResources().getStringArray(R.array.voice_input_modes_summary)
+ [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case VOICE_INPUT_CONFIRM_DIALOG:
+ DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (whichButton == DialogInterface.BUTTON_NEGATIVE) {
+ mVoicePreference.setValue(mVoiceModeOff);
+ mVoiceLogger.settingsWarningDialogCancel();
+ } else if (whichButton == DialogInterface.BUTTON_POSITIVE) {
+ mOkClicked = true;
+ mVoiceLogger.settingsWarningDialogOk();
+ }
+ updateVoicePreference();
+ }
+ };
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.voice_warning_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setNegativeButton(android.R.string.cancel, listener);
+
+ // Get the current list of supported locales and check the current locale against
+ // that list, to decide whether to put a warning that voice input will not work in
+ // the current language as part of the pop-up confirmation dialog.
+ boolean localeSupported = SubtypeSwitcher.getInstance().isVoiceSupported(
+ Locale.getDefault().toString());
+
+ final CharSequence message;
+ if (localeSupported) {
+ message = TextUtils.concat(
+ getText(R.string.voice_warning_may_not_understand), "\n\n",
+ getText(R.string.voice_hint_dialog_message));
+ } else {
+ message = TextUtils.concat(
+ getText(R.string.voice_warning_locale_not_supported), "\n\n",
+ getText(R.string.voice_warning_may_not_understand), "\n\n",
+ getText(R.string.voice_hint_dialog_message));
+ }
+ builder.setMessage(message);
+ AlertDialog dialog = builder.create();
+ mDialog = dialog;
+ dialog.setOnDismissListener(this);
+ mVoiceLogger.settingsWarningDialogShown();
+ return dialog;
+ default:
+ Log.e(TAG, "unknown dialog " + id);
+ return null;
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mVoiceLogger.settingsWarningDialogDismissed();
+ if (!mOkClicked) {
+ // This assumes that onPreferenceClick gets called first, and this if the user
+ // agreed after the warning, we set the mOkClicked value to true.
+ mVoicePreference.setValue(mVoiceModeOff);
+ }
+ }
+
+ private void updateVoicePreference() {
+ boolean isChecked = !mVoicePreference.getValue().equals(mVoiceModeOff);
+ mVoiceLogger.voiceInputSettingEnabled(isChecked);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
new file mode 100644
index 000000000..917521c40
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
@@ -0,0 +1,46 @@
+/*
+ * 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.latin;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import java.util.Locale;
+
+public class SubtypeLocale {
+ private static String[] sExceptionKeys;
+ private static String[] sExceptionValues;
+
+ private SubtypeLocale() {
+ // Intentional empty constructor for utility class.
+ }
+
+ public static void init(Context context) {
+ final Resources res = context.getResources();
+ sExceptionKeys = res.getStringArray(R.array.subtype_locale_exception_keys);
+ sExceptionValues = res.getStringArray(R.array.subtype_locale_exception_values);
+ }
+
+ public static String getFullDisplayName(Locale locale) {
+ String localeCode = locale.toString();
+ for (int index = 0; index < sExceptionKeys.length; index++) {
+ if (sExceptionKeys[index].equals(localeCode))
+ return sExceptionValues[index];
+ }
+ return locale.getDisplayName(locale);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
new file mode 100644
index 000000000..6ca12c0c5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -0,0 +1,674 @@
+/*
+ * 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.latin;
+
+import com.android.inputmethod.compat.InputMethodInfoCompatWrapper;
+import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper;
+import com.android.inputmethod.deprecated.VoiceProxy;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.LatinKeyboard;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class SubtypeSwitcher {
+ private static boolean DBG = LatinImeLogger.sDBG;
+ private static final String TAG = SubtypeSwitcher.class.getSimpleName();
+
+ private static final char LOCALE_SEPARATER = '_';
+ private static final String KEYBOARD_MODE = "keyboard";
+ private static final String VOICE_MODE = "voice";
+ private static final String SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY =
+ "requireNetworkConnectivity";
+ public static final String USE_SPACEBAR_LANGUAGE_SWITCH_KEY = "use_spacebar_language_switch";
+
+ private final TextUtils.SimpleStringSplitter mLocaleSplitter =
+ new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER);
+
+ private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
+ private /* final */ LatinIME mService;
+ private /* final */ InputMethodManagerCompatWrapper mImm;
+ private /* final */ Resources mResources;
+ private /* final */ ConnectivityManager mConnectivityManager;
+ private /* final */ boolean mConfigUseSpacebarLanguageSwitcher;
+ private /* final */ SharedPreferences mPrefs;
+ private final ArrayList<InputMethodSubtypeCompatWrapper>
+ mEnabledKeyboardSubtypesOfCurrentInputMethod =
+ new ArrayList<InputMethodSubtypeCompatWrapper>();
+ private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>();
+ private final LanguageBarInfo mLanguageBarInfo = new LanguageBarInfo();
+
+ /*-----------------------------------------------------------*/
+ // Variants which should be changed only by reload functions.
+ private boolean mNeedsToDisplayLanguage;
+ private boolean mIsSystemLanguageSameAsInputLanguage;
+ private InputMethodInfoCompatWrapper mShortcutInputMethodInfo;
+ private InputMethodSubtypeCompatWrapper mShortcutSubtype;
+ private List<InputMethodSubtypeCompatWrapper> mAllEnabledSubtypesOfCurrentInputMethod;
+ private InputMethodSubtypeCompatWrapper mCurrentSubtype;
+ private Locale mSystemLocale;
+ private Locale mInputLocale;
+ private String mInputLocaleStr;
+ private String mInputMethodId;
+ private VoiceProxy.VoiceInputWrapper mVoiceInputWrapper;
+ /*-----------------------------------------------------------*/
+
+ private boolean mIsNetworkConnected;
+
+ public static SubtypeSwitcher getInstance() {
+ return sInstance;
+ }
+
+ public static void init(LatinIME service, SharedPreferences prefs) {
+ SubtypeLocale.init(service);
+ sInstance.initialize(service, prefs);
+ sInstance.updateAllParameters();
+ }
+
+ private SubtypeSwitcher() {
+ // Intentional empty constructor for singleton.
+ }
+
+ private void initialize(LatinIME service, SharedPreferences prefs) {
+ mService = service;
+ mResources = service.getResources();
+ mImm = InputMethodManagerCompatWrapper.getInstance(service);
+ mConnectivityManager = (ConnectivityManager) service.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ mEnabledKeyboardSubtypesOfCurrentInputMethod.clear();
+ mEnabledLanguagesOfCurrentInputMethod.clear();
+ mSystemLocale = null;
+ mInputLocale = null;
+ mInputLocaleStr = null;
+ mCurrentSubtype = null;
+ mAllEnabledSubtypesOfCurrentInputMethod = null;
+ mVoiceInputWrapper = null;
+ mPrefs = prefs;
+
+ final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
+ mIsNetworkConnected = (info != null && info.isConnected());
+ mInputMethodId = Utils.getInputMethodId(mImm, service.getPackageName());
+ }
+
+ // Update all parameters stored in SubtypeSwitcher.
+ // Only configuration changed event is allowed to call this because this is heavy.
+ private void updateAllParameters() {
+ mSystemLocale = mResources.getConfiguration().locale;
+ updateSubtype(mImm.getCurrentInputMethodSubtype());
+ updateParametersOnStartInputView();
+ }
+
+ // Update parameters which are changed outside LatinIME. This parameters affect UI so they
+ // should be updated every time onStartInputview.
+ public void updateParametersOnStartInputView() {
+ mConfigUseSpacebarLanguageSwitcher = mPrefs.getBoolean(USE_SPACEBAR_LANGUAGE_SWITCH_KEY,
+ mService.getResources().getBoolean(
+ R.bool.config_use_spacebar_language_switcher));
+ updateEnabledSubtypes();
+ updateShortcutIME();
+ }
+
+ // Reload enabledSubtypes from the framework.
+ private void updateEnabledSubtypes() {
+ final String currentMode = getCurrentSubtypeMode();
+ boolean foundCurrentSubtypeBecameDisabled = true;
+ mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList(
+ null, true);
+ mEnabledLanguagesOfCurrentInputMethod.clear();
+ mEnabledKeyboardSubtypesOfCurrentInputMethod.clear();
+ for (InputMethodSubtypeCompatWrapper ims : mAllEnabledSubtypesOfCurrentInputMethod) {
+ final String locale = ims.getLocale();
+ final String mode = ims.getMode();
+ mLocaleSplitter.setString(locale);
+ if (mLocaleSplitter.hasNext()) {
+ mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next());
+ }
+ if (locale.equals(mInputLocaleStr) && mode.equals(currentMode)) {
+ foundCurrentSubtypeBecameDisabled = false;
+ }
+ if (KEYBOARD_MODE.equals(ims.getMode())) {
+ mEnabledKeyboardSubtypesOfCurrentInputMethod.add(ims);
+ }
+ }
+ mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
+ && mIsSystemLanguageSameAsInputLanguage);
+ if (foundCurrentSubtypeBecameDisabled) {
+ if (DBG) {
+ Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + currentMode);
+ Log.w(TAG, "Last subtype was disabled. Update to the current one.");
+ }
+ updateSubtype(mImm.getCurrentInputMethodSubtype());
+ } else {
+ // mLanguageBarInfo.update() will be called in updateSubtype so there is no need
+ // to call this in the if-clause above.
+ mLanguageBarInfo.update();
+ }
+ }
+
+ private void updateShortcutIME() {
+ if (DBG) {
+ Log.d(TAG, "Update shortcut IME from : "
+ + (mShortcutInputMethodInfo == null
+ ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+ + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale()
+ + ", " + mShortcutSubtype.getMode())));
+ }
+ // TODO: Update an icon for shortcut IME
+ final Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcuts =
+ mImm.getShortcutInputMethodsAndSubtypes();
+ for (InputMethodInfoCompatWrapper imi : shortcuts.keySet()) {
+ List<InputMethodSubtypeCompatWrapper> subtypes = shortcuts.get(imi);
+ // TODO: Returns the first found IMI for now. Should handle all shortcuts as
+ // appropriate.
+ mShortcutInputMethodInfo = imi;
+ // TODO: Pick up the first found subtype for now. Should handle all subtypes
+ // as appropriate.
+ mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
+ break;
+ }
+ if (DBG) {
+ Log.d(TAG, "Update shortcut IME to : "
+ + (mShortcutInputMethodInfo == null
+ ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+ + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale()
+ + ", " + mShortcutSubtype.getMode())));
+ }
+ }
+
+ // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
+ public void updateSubtype(InputMethodSubtypeCompatWrapper newSubtype) {
+ final String newLocale;
+ final String newMode;
+ final String oldMode = getCurrentSubtypeMode();
+ if (newSubtype == null) {
+ // Normally, newSubtype shouldn't be null. But just in case newSubtype was null,
+ // fallback to the default locale.
+ Log.w(TAG, "Couldn't get the current subtype.");
+ newLocale = "en_US";
+ newMode = KEYBOARD_MODE;
+ } else {
+ newLocale = newSubtype.getLocale();
+ newMode = newSubtype.getMode();
+ }
+ if (DBG) {
+ Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode
+ + ", from: " + mInputLocaleStr + ", " + oldMode);
+ }
+ boolean languageChanged = false;
+ if (!newLocale.equals(mInputLocaleStr)) {
+ if (mInputLocaleStr != null) {
+ languageChanged = true;
+ }
+ updateInputLocale(newLocale);
+ }
+ boolean modeChanged = false;
+ if (!newMode.equals(oldMode)) {
+ if (oldMode != null) {
+ modeChanged = true;
+ }
+ }
+ mCurrentSubtype = newSubtype;
+
+ // If the old mode is voice input, we need to reset or cancel its status.
+ // We cancel its status when we change mode, while we reset otherwise.
+ if (isKeyboardMode()) {
+ if (modeChanged) {
+ if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) {
+ mVoiceInputWrapper.cancel();
+ }
+ }
+ if (modeChanged || languageChanged) {
+ updateShortcutIME();
+ mService.onRefreshKeyboard();
+ }
+ } else if (isVoiceMode() && mVoiceInputWrapper != null) {
+ if (VOICE_MODE.equals(oldMode)) {
+ mVoiceInputWrapper.reset();
+ }
+ // If needsToShowWarningDialog is true, voice input need to show warning before
+ // show recognition view.
+ if (languageChanged || modeChanged
+ || VoiceProxy.getInstance().needsToShowWarningDialog()) {
+ triggerVoiceIME();
+ }
+ } else {
+ Log.w(TAG, "Unknown subtype mode: " + newMode);
+ if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) {
+ // We need to reset the voice input to release the resources and to reset its status
+ // as it is not the current input mode.
+ mVoiceInputWrapper.reset();
+ }
+ }
+ mLanguageBarInfo.update();
+ }
+
+ // Update the current input locale from Locale string.
+ private void updateInputLocale(String inputLocaleStr) {
+ // example: inputLocaleStr = "en_US" "en" ""
+ // "en_US" --> language: en & country: US
+ // "en" --> language: en
+ // "" --> the system locale
+ if (!TextUtils.isEmpty(inputLocaleStr)) {
+ mInputLocale = Utils.constructLocaleFromString(inputLocaleStr);
+ mInputLocaleStr = inputLocaleStr;
+ } else {
+ mInputLocale = mSystemLocale;
+ String country = mSystemLocale.getCountry();
+ mInputLocaleStr = mSystemLocale.getLanguage()
+ + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage());
+ }
+ mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase(
+ getInputLocale().getLanguage());
+ mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
+ && mIsSystemLanguageSameAsInputLanguage);
+ }
+
+ ////////////////////////////
+ // Shortcut IME functions //
+ ////////////////////////////
+
+ public void switchToShortcutIME() {
+ if (mShortcutInputMethodInfo == null) {
+ return;
+ }
+
+ final String imiId = mShortcutInputMethodInfo.getId();
+ final InputMethodSubtypeCompatWrapper subtype = mShortcutSubtype;
+ switchToTargetIME(imiId, subtype);
+ }
+
+ private void switchToTargetIME(
+ final String imiId, final InputMethodSubtypeCompatWrapper subtype) {
+ final IBinder token = mService.getWindow().getWindow().getAttributes().token;
+ if (token == null) {
+ return;
+ }
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ mImm.setInputMethodAndSubtype(token, imiId, subtype);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ // Calls in this method need to be done in the same thread as the thread which
+ // called switchToShortcutIME().
+
+ // 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(subtype);
+ }
+ }.execute();
+ }
+
+ public Drawable getShortcutIcon() {
+ return getSubtypeIcon(mShortcutInputMethodInfo, mShortcutSubtype);
+ }
+
+ private Drawable getSubtypeIcon(
+ InputMethodInfoCompatWrapper imi, InputMethodSubtypeCompatWrapper subtype) {
+ final PackageManager pm = mService.getPackageManager();
+ if (imi != null) {
+ final String imiPackageName = imi.getPackageName();
+ if (DBG) {
+ Log.d(TAG, "Update icons of IME: " + imiPackageName + ","
+ + subtype.getLocale() + "," + subtype.getMode());
+ }
+ if (subtype != null) {
+ return pm.getDrawable(imiPackageName, subtype.getIconResId(),
+ imi.getServiceInfo().applicationInfo);
+ } else if (imi.getSubtypeCount() > 0 && imi.getSubtypeAt(0) != null) {
+ return pm.getDrawable(imiPackageName,
+ imi.getSubtypeAt(0).getIconResId(),
+ imi.getServiceInfo().applicationInfo);
+ } else {
+ try {
+ return pm.getApplicationInfo(imiPackageName, 0).loadIcon(pm);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "IME can't be found: " + imiPackageName);
+ }
+ }
+ }
+ return null;
+ }
+
+ private static boolean contains(String[] hay, String needle) {
+ for (String element : hay) {
+ if (element.equals(needle))
+ return true;
+ }
+ return false;
+ }
+
+ public boolean isShortcutImeEnabled() {
+ if (mShortcutInputMethodInfo == null)
+ return false;
+ if (mShortcutSubtype == null)
+ return true;
+ // For compatibility, if the shortcut subtype is dummy, we assume the shortcut IME
+ // (built-in voice dummy subtype) is available.
+ if (!mShortcutSubtype.hasOriginalObject()) return true;
+ final boolean allowsImplicitlySelectedSubtypes = true;
+ for (final InputMethodSubtypeCompatWrapper enabledSubtype :
+ mImm.getEnabledInputMethodSubtypeList(
+ mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) {
+ if (enabledSubtype.equals(mShortcutSubtype)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isShortcutImeReady() {
+ if (mShortcutInputMethodInfo == null)
+ return false;
+ if (mShortcutSubtype == null)
+ return true;
+ if (contains(mShortcutSubtype.getExtraValue().split(","),
+ SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) {
+ return mIsNetworkConnected;
+ }
+ return true;
+ }
+
+ public void onNetworkStateChanged(Intent intent) {
+ final boolean noConnection = intent.getBooleanExtra(
+ ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+ mIsNetworkConnected = !noConnection;
+
+ final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
+ final LatinKeyboard keyboard = switcher.getLatinKeyboard();
+ if (keyboard != null) {
+ keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getKeyboardView());
+ }
+ }
+
+ //////////////////////////////////
+ // Language Switching functions //
+ //////////////////////////////////
+
+ public int getEnabledKeyboardLocaleCount() {
+ return mEnabledKeyboardSubtypesOfCurrentInputMethod.size();
+ }
+
+ public boolean useSpacebarLanguageSwitcher() {
+ return mConfigUseSpacebarLanguageSwitcher;
+ }
+
+ public boolean needsToDisplayLanguage() {
+ return mNeedsToDisplayLanguage;
+ }
+
+ public Locale getInputLocale() {
+ return mInputLocale;
+ }
+
+ public String getInputLocaleStr() {
+ return mInputLocaleStr;
+ }
+
+ public String[] getEnabledLanguages() {
+ int enabledLanguageCount = mEnabledLanguagesOfCurrentInputMethod.size();
+ // Workaround for explicitly specifying the voice language
+ if (enabledLanguageCount == 1) {
+ mEnabledLanguagesOfCurrentInputMethod.add(mEnabledLanguagesOfCurrentInputMethod
+ .get(0));
+ ++enabledLanguageCount;
+ }
+ return mEnabledLanguagesOfCurrentInputMethod.toArray(new String[enabledLanguageCount]);
+ }
+
+ public Locale getSystemLocale() {
+ return mSystemLocale;
+ }
+
+ public boolean isSystemLanguageSameAsInputLanguage() {
+ return mIsSystemLanguageSameAsInputLanguage;
+ }
+
+ public void onConfigurationChanged(Configuration conf) {
+ final Locale systemLocale = conf.locale;
+ // If system configuration was changed, update all parameters.
+ if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) {
+ updateAllParameters();
+ }
+ }
+
+ public boolean isKeyboardMode() {
+ return KEYBOARD_MODE.equals(getCurrentSubtypeMode());
+ }
+
+
+ ///////////////////////////
+ // Voice Input functions //
+ ///////////////////////////
+
+ public boolean setVoiceInputWrapper(VoiceProxy.VoiceInputWrapper vi) {
+ if (mVoiceInputWrapper == null && vi != null) {
+ mVoiceInputWrapper = vi;
+ if (isVoiceMode()) {
+ if (DBG) {
+ Log.d(TAG, "Set and call voice input.: " + getInputLocaleStr());
+ }
+ triggerVoiceIME();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isVoiceMode() {
+ return null == mCurrentSubtype ? false : VOICE_MODE.equals(getCurrentSubtypeMode());
+ }
+
+ public boolean isDummyVoiceMode() {
+ return mCurrentSubtype != null && mCurrentSubtype.getOriginalObject() == null
+ && VOICE_MODE.equals(getCurrentSubtypeMode());
+ }
+
+ private void triggerVoiceIME() {
+ if (!mService.isInputViewShown()) return;
+ VoiceProxy.getInstance().startListening(false,
+ KeyboardSwitcher.getInstance().getKeyboardView().getWindowToken());
+ }
+
+ //////////////////////////////////////
+ // Spacebar Language Switch support //
+ //////////////////////////////////////
+
+ private class LanguageBarInfo {
+ private int mCurrentKeyboardSubtypeIndex;
+ private InputMethodSubtypeCompatWrapper mNextKeyboardSubtype;
+ private InputMethodSubtypeCompatWrapper mPreviousKeyboardSubtype;
+ private String mNextLanguage;
+ private String mPreviousLanguage;
+ public LanguageBarInfo() {
+ update();
+ }
+
+ private String getNextLanguage() {
+ return mNextLanguage;
+ }
+
+ private String getPreviousLanguage() {
+ return mPreviousLanguage;
+ }
+
+ public InputMethodSubtypeCompatWrapper getNextKeyboardSubtype() {
+ return mNextKeyboardSubtype;
+ }
+
+ public InputMethodSubtypeCompatWrapper getPreviousKeyboardSubtype() {
+ return mPreviousKeyboardSubtype;
+ }
+
+ public void update() {
+ if (!mConfigUseSpacebarLanguageSwitcher
+ || mEnabledKeyboardSubtypesOfCurrentInputMethod == null
+ || mEnabledKeyboardSubtypesOfCurrentInputMethod.size() == 0) return;
+ mCurrentKeyboardSubtypeIndex = getCurrentIndex();
+ mNextKeyboardSubtype = getNextKeyboardSubtypeInternal(mCurrentKeyboardSubtypeIndex);
+ Locale locale = Utils.constructLocaleFromString(mNextKeyboardSubtype.getLocale());
+ mNextLanguage = getFullDisplayName(locale, true);
+ mPreviousKeyboardSubtype = getPreviousKeyboardSubtypeInternal(
+ mCurrentKeyboardSubtypeIndex);
+ locale = Utils.constructLocaleFromString(mPreviousKeyboardSubtype.getLocale());
+ mPreviousLanguage = getFullDisplayName(locale, true);
+ }
+
+ private int normalize(int index) {
+ final int N = mEnabledKeyboardSubtypesOfCurrentInputMethod.size();
+ final int ret = index % N;
+ return ret < 0 ? ret + N : ret;
+ }
+
+ private int getCurrentIndex() {
+ final int N = mEnabledKeyboardSubtypesOfCurrentInputMethod.size();
+ for (int i = 0; i < N; ++i) {
+ if (mEnabledKeyboardSubtypesOfCurrentInputMethod.get(i).equals(mCurrentSubtype)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ private InputMethodSubtypeCompatWrapper getNextKeyboardSubtypeInternal(int index) {
+ return mEnabledKeyboardSubtypesOfCurrentInputMethod.get(normalize(index + 1));
+ }
+
+ private InputMethodSubtypeCompatWrapper getPreviousKeyboardSubtypeInternal(int index) {
+ return mEnabledKeyboardSubtypesOfCurrentInputMethod.get(normalize(index - 1));
+ }
+ }
+
+ public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) {
+ if (returnsNameInThisLocale) {
+ return toTitleCase(SubtypeLocale.getFullDisplayName(locale), locale);
+ } else {
+ return toTitleCase(locale.getDisplayName(), locale);
+ }
+ }
+
+ public static String getDisplayLanguage(Locale locale) {
+ return toTitleCase(SubtypeLocale.getFullDisplayName(locale), locale);
+ }
+
+ public static String getMiddleDisplayLanguage(Locale locale) {
+ return toTitleCase((Utils.constructLocaleFromString(
+ locale.getLanguage()).getDisplayLanguage(locale)), locale);
+ }
+
+ public static String getShortDisplayLanguage(Locale locale) {
+ return toTitleCase(locale.getLanguage(), locale);
+ }
+
+ private static String toTitleCase(String s, Locale locale) {
+ if (s.length() == 0) {
+ return s;
+ }
+ return s.toUpperCase(locale).charAt(0) + s.substring(1);
+ }
+
+ public String getInputLanguageName() {
+ return getDisplayLanguage(getInputLocale());
+ }
+
+ public String getNextInputLanguageName() {
+ return mLanguageBarInfo.getNextLanguage();
+ }
+
+ public String getPreviousInputLanguageName() {
+ return mLanguageBarInfo.getPreviousLanguage();
+ }
+
+ /////////////////////////////
+ // Other utility functions //
+ /////////////////////////////
+
+ public String getCurrentSubtypeExtraValue() {
+ // If null, return what an empty ExtraValue would return : the empty string.
+ return null != mCurrentSubtype ? mCurrentSubtype.getExtraValue() : "";
+ }
+
+ public boolean currentSubtypeContainsExtraValueKey(String key) {
+ // If null, return what an empty ExtraValue would return : false.
+ return null != mCurrentSubtype ? mCurrentSubtype.containsExtraValueKey(key) : false;
+ }
+
+ public String getCurrentSubtypeExtraValueOf(String key) {
+ // If null, return what an empty ExtraValue would return : null.
+ return null != mCurrentSubtype ? mCurrentSubtype.getExtraValueOf(key) : null;
+ }
+
+ public String getCurrentSubtypeMode() {
+ return null != mCurrentSubtype ? mCurrentSubtype.getMode() : KEYBOARD_MODE;
+ }
+
+
+ public boolean isVoiceSupported(String locale) {
+ // Get the current list of supported locales and check the current locale against that
+ // list. We cache this value so as not to check it every time the user starts a voice
+ // input. Because this method is called by onStartInputView, this should mean that as
+ // long as the locale doesn't change while the user is keeping the IME open, the
+ // value should never be stale.
+ String supportedLocalesString = VoiceProxy.getSupportedLocalesString(
+ mService.getContentResolver());
+ List<String> voiceInputSupportedLocales = Arrays.asList(
+ supportedLocalesString.split("\\s+"));
+ return voiceInputSupportedLocales.contains(locale);
+ }
+
+ private void changeToNextSubtype() {
+ final InputMethodSubtypeCompatWrapper subtype =
+ mLanguageBarInfo.getNextKeyboardSubtype();
+ switchToTargetIME(mInputMethodId, subtype);
+ }
+
+ private void changeToPreviousSubtype() {
+ final InputMethodSubtypeCompatWrapper subtype =
+ mLanguageBarInfo.getPreviousKeyboardSubtype();
+ switchToTargetIME(mInputMethodId, subtype);
+ }
+
+ public void toggleLanguage(boolean next) {
+ if (next) {
+ changeToNextSubtype();
+ } else {
+ changeToPreviousSubtype();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 5015e9b3d..eb5ed5a65 100755..100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -22,18 +22,23 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.View;
-import java.nio.ByteBuffer;
+import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.List;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
/**
- * This class loads a dictionary and provides a list of suggestions for a given sequence of
+ * This class loads a dictionary and provides a list of suggestions for a given sequence of
* characters. This includes corrections and completions.
- * @hide pending API Council Approval
*/
public class Suggest implements Dictionary.WordCallback {
+ public static final String TAG = Suggest.class.getSimpleName();
+
public static final int APPROX_MAX_WORD_LENGTH = 32;
public static final int CORRECTION_NONE = 0;
@@ -43,7 +48,7 @@ public class Suggest implements Dictionary.WordCallback {
/**
* Words that appear in both bigram and unigram data gets multiplier ranging from
- * BIGRAM_MULTIPLIER_MIN to BIGRAM_MULTIPLIER_MAX depending on the frequency score from
+ * BIGRAM_MULTIPLIER_MIN to BIGRAM_MULTIPLIER_MAX depending on the score from
* bigram data.
*/
public static final double BIGRAM_MULTIPLIER_MIN = 1.2;
@@ -51,7 +56,7 @@ public class Suggest implements Dictionary.WordCallback {
/**
* Maximum possible bigram frequency. Will depend on how many bits are being used in data
- * structure. Maximum bigram freqeuncy will get the BIGRAM_MULTIPLIER_MAX as the multiplier.
+ * structure. Maximum bigram frequency will get the BIGRAM_MULTIPLIER_MAX as the multiplier.
*/
public static final int MAXIMUM_BIGRAM_FREQUENCY = 127;
@@ -63,39 +68,36 @@ public class Suggest implements Dictionary.WordCallback {
// If you add a type of dictionary, increment DIC_TYPE_LAST_ID
public static final int DIC_TYPE_LAST_ID = 4;
- static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000;
-
- private BinaryDictionary mMainDict;
-
- private Dictionary mUserDictionary;
+ public static final String DICT_KEY_MAIN = "main";
+ public static final String DICT_KEY_CONTACTS = "contacts";
+ public static final String DICT_KEY_AUTO = "auto";
+ public static final String DICT_KEY_USER = "user";
+ public static final String DICT_KEY_USER_BIGRAM = "user_bigram";
+ public static final String DICT_KEY_WHITELIST ="whitelist";
- private Dictionary mAutoDictionary;
+ private static final boolean DBG = LatinImeLogger.sDBG;
- private Dictionary mContactsDictionary;
+ private AutoCorrection mAutoCorrection;
- private Dictionary mUserBigramDictionary;
+ private Dictionary mMainDict;
+ private WhitelistDictionary mWhiteListDictionary;
+ private final Map<String, Dictionary> mUnigramDictionaries = new HashMap<String, Dictionary>();
+ private final Map<String, Dictionary> mBigramDictionaries = new HashMap<String, Dictionary>();
- private int mPrefMaxSuggestions = 12;
+ private int mPrefMaxSuggestions = 18;
private static final int PREF_MAX_BIGRAMS = 60;
- private boolean mAutoTextEnabled;
+ private boolean mQuickFixesEnabled;
- private int[] mPriorities = new int[mPrefMaxSuggestions];
- private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS];
+ private double mAutoCorrectionThreshold;
+ private int[] mScores = new int[mPrefMaxSuggestions];
+ private int[] mBigramScores = new int[PREF_MAX_BIGRAMS];
- // Handle predictive correction for only the first 1280 characters for performance reasons
- // If we support scripts that need latin characters beyond that, we should probably use some
- // kind of a sparse array or language specific list with a mapping lookup table.
- // 1280 is the size of the BASE_CHARS array in ExpandableDictionary, which is a basic set of
- // latin characters.
- private int[] mNextLettersFrequencies = new int[1280];
private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
ArrayList<CharSequence> mBigramSuggestions = new ArrayList<CharSequence>();
private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
- private boolean mHaveCorrection;
- private CharSequence mOriginalWord;
- private String mLowerOriginalWord;
+ private CharSequence mTypedWord;
// TODO: Remove these member variables by passing more context to addWord() callback method
private boolean mIsFirstCharCapitalized;
@@ -103,16 +105,45 @@ public class Suggest implements Dictionary.WordCallback {
private int mCorrectionMode = CORRECTION_BASIC;
- public Suggest(Context context, int[] dictionaryResId) {
- mMainDict = new BinaryDictionary(context, dictionaryResId, DIC_MAIN);
- initPool();
+ public Suggest(Context context, int dictionaryResId, Locale locale) {
+ init(context, DictionaryFactory.createDictionaryFromManager(context, locale,
+ dictionaryResId));
+ }
+
+ /* package for test */ Suggest(Context context, File dictionary, long startOffset, long length,
+ Flag[] flagArray) {
+ init(null, DictionaryFactory.createDictionaryForTest(context, dictionary, startOffset,
+ length, flagArray));
}
- public Suggest(Context context, ByteBuffer byteBuffer) {
- mMainDict = new BinaryDictionary(context, byteBuffer, DIC_MAIN);
+ private void init(Context context, Dictionary mainDict) {
+ mMainDict = mainDict;
+ addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict);
+ addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict);
+ mWhiteListDictionary = WhitelistDictionary.init(context);
+ addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_WHITELIST, mWhiteListDictionary);
+ mAutoCorrection = new AutoCorrection();
initPool();
}
+ private void addOrReplaceDictionary(Map<String, Dictionary> dictionaries, String key,
+ Dictionary dict) {
+ final Dictionary oldDict = (dict == null)
+ ? dictionaries.remove(key)
+ : dictionaries.put(key, dict);
+ if (oldDict != null && dict != oldDict) {
+ oldDict.close();
+ }
+ }
+
+ public void resetMainDict(Context context, int dictionaryResId, Locale locale) {
+ final Dictionary newMainDict = DictionaryFactory.createDictionaryFromManager(
+ context, locale, dictionaryResId);
+ mMainDict = newMainDict;
+ addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
+ addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
+ }
+
private void initPool() {
for (int i = 0; i < mPrefMaxSuggestions; i++) {
StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
@@ -120,8 +151,8 @@ public class Suggest implements Dictionary.WordCallback {
}
}
- public void setAutoTextEnabled(boolean enabled) {
- mAutoTextEnabled = enabled;
+ public void setQuickFixesEnabled(boolean enabled) {
+ mQuickFixesEnabled = enabled;
}
public int getCorrectionMode() {
@@ -133,7 +164,11 @@ public class Suggest implements Dictionary.WordCallback {
}
public boolean hasMainDictionary() {
- return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD;
+ return mMainDict != null;
+ }
+
+ public Map<String, Dictionary> getUnigramDictionaries() {
+ return mUnigramDictionaries;
}
public int getApproxMaxWordLength() {
@@ -145,22 +180,33 @@ public class Suggest implements Dictionary.WordCallback {
* before the main dictionary, if set.
*/
public void setUserDictionary(Dictionary userDictionary) {
- mUserDictionary = userDictionary;
+ addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER, userDictionary);
}
/**
- * Sets an optional contacts dictionary resource to be loaded.
+ * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove
+ * the contacts dictionary by passing null to this method. In this case no contacts dictionary
+ * won't be used.
*/
- public void setContactsDictionary(Dictionary userDictionary) {
- mContactsDictionary = userDictionary;
+ public void setContactsDictionary(Dictionary contactsDictionary) {
+ addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
+ addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
}
-
+
public void setAutoDictionary(Dictionary autoDictionary) {
- mAutoDictionary = autoDictionary;
+ addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_AUTO, autoDictionary);
}
public void setUserBigramDictionary(Dictionary userBigramDictionary) {
- mUserBigramDictionary = userBigramDictionary;
+ addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_BIGRAM, userBigramDictionary);
+ }
+
+ public void setAutoCorrectionThreshold(double threshold) {
+ mAutoCorrectionThreshold = threshold;
+ }
+
+ public boolean isAggressiveAutoCorrectionMode() {
+ return (mAutoCorrectionThreshold == 0);
}
/**
@@ -174,8 +220,8 @@ public class Suggest implements Dictionary.WordCallback {
throw new IllegalArgumentException("maxSuggestions must be between 1 and 100");
}
mPrefMaxSuggestions = maxSuggestions;
- mPriorities = new int[mPrefMaxSuggestions];
- mBigramPriorities = new int[PREF_MAX_BIGRAMS];
+ mScores = new int[mPrefMaxSuggestions];
+ mBigramScores = new int[PREF_MAX_BIGRAMS];
collectGarbage(mSuggestions, mPrefMaxSuggestions);
while (mStringPool.size() < mPrefMaxSuggestions) {
StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
@@ -183,172 +229,190 @@ public class Suggest implements Dictionary.WordCallback {
}
}
- private boolean haveSufficientCommonality(String original, CharSequence suggestion) {
- final int originalLength = original.length();
- final int suggestionLength = suggestion.length();
- final int minLength = Math.min(originalLength, suggestionLength);
- if (minLength <= 2) return true;
- int matching = 0;
- int lessMatching = 0; // Count matches if we skip one character
- int i;
- for (i = 0; i < minLength; i++) {
- final char origChar = ExpandableDictionary.toLowerCase(original.charAt(i));
- if (origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i))) {
- matching++;
- lessMatching++;
- } else if (i + 1 < suggestionLength
- && origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i + 1))) {
- lessMatching++;
- }
- }
- matching = Math.max(matching, lessMatching);
-
- if (minLength <= 4) {
- return matching >= 2;
- } else {
- return matching > minLength / 2;
- }
- }
-
/**
- * Returns a list of words that match the list of character codes passed in.
- * This list will be overwritten the next time this function is called.
+ * Returns a object which represents suggested words that match the list of character codes
+ * passed in. This object contents will be overwritten the next time this function is called.
* @param view a view for retrieving the context for AutoText
* @param wordComposer contains what is currently being typed
* @param prevWordForBigram previous word (used only for bigram)
- * @return list of suggestions.
+ * @return suggested words object.
*/
- public List<CharSequence> getSuggestions(View view, WordComposer wordComposer,
- boolean includeTypedWordIfValid, CharSequence prevWordForBigram) {
+ public SuggestedWords getSuggestions(View view, WordComposer wordComposer,
+ CharSequence prevWordForBigram) {
+ return getSuggestedWordBuilder(view, wordComposer, prevWordForBigram).build();
+ }
+
+ private CharSequence capitalizeWord(boolean all, boolean first, CharSequence word) {
+ if (TextUtils.isEmpty(word) || !(all || first)) return word;
+ final int wordLength = word.length();
+ final int poolSize = mStringPool.size();
+ final StringBuilder sb =
+ poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
+ : new StringBuilder(getApproxMaxWordLength());
+ sb.setLength(0);
+ // TODO: Must pay attention to locale when changing case.
+ if (all) {
+ sb.append(word.toString().toUpperCase());
+ } else if (first) {
+ sb.append(Character.toUpperCase(word.charAt(0)));
+ if (wordLength > 1) {
+ sb.append(word.subSequence(1, wordLength));
+ }
+ }
+ return sb;
+ }
+
+ protected void addBigramToSuggestions(CharSequence bigram) {
+ final int poolSize = mStringPool.size();
+ final StringBuilder sb = poolSize > 0 ?
+ (StringBuilder) mStringPool.remove(poolSize - 1)
+ : new StringBuilder(getApproxMaxWordLength());
+ sb.setLength(0);
+ sb.append(bigram);
+ mSuggestions.add(sb);
+ }
+
+ // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
+ public SuggestedWords.Builder getSuggestedWordBuilder(View view, WordComposer wordComposer,
+ CharSequence prevWordForBigram) {
LatinImeLogger.onStartSuggestion(prevWordForBigram);
- mHaveCorrection = false;
+ mAutoCorrection.init();
mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
mIsAllUpperCase = wordComposer.isAllUpperCase();
collectGarbage(mSuggestions, mPrefMaxSuggestions);
- Arrays.fill(mPriorities, 0);
- Arrays.fill(mNextLettersFrequencies, 0);
+ Arrays.fill(mScores, 0);
// Save a lowercase version of the original word
- mOriginalWord = wordComposer.getTypedWord();
- if (mOriginalWord != null) {
- final String mOriginalWordString = mOriginalWord.toString();
- mOriginalWord = mOriginalWordString;
- mLowerOriginalWord = mOriginalWordString.toLowerCase();
+ CharSequence typedWord = wordComposer.getTypedWord();
+ if (typedWord != null) {
+ final String typedWordString = typedWord.toString();
+ typedWord = typedWordString;
// Treating USER_TYPED as UNIGRAM suggestion for logging now.
- LatinImeLogger.onAddSuggestedWord(mOriginalWordString, Suggest.DIC_USER_TYPED,
+ LatinImeLogger.onAddSuggestedWord(typedWordString, Suggest.DIC_USER_TYPED,
Dictionary.DataType.UNIGRAM);
- } else {
- mLowerOriginalWord = "";
}
+ mTypedWord = typedWord;
- if (wordComposer.size() == 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM
+ if (wordComposer.size() <= 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM
|| mCorrectionMode == CORRECTION_BASIC)) {
// At first character typed, search only the bigrams
- Arrays.fill(mBigramPriorities, 0);
+ Arrays.fill(mBigramScores, 0);
collectGarbage(mBigramSuggestions, PREF_MAX_BIGRAMS);
if (!TextUtils.isEmpty(prevWordForBigram)) {
CharSequence lowerPrevWord = prevWordForBigram.toString().toLowerCase();
- if (mMainDict.isValidWord(lowerPrevWord)) {
+ if (mMainDict != null && mMainDict.isValidWord(lowerPrevWord)) {
prevWordForBigram = lowerPrevWord;
}
- if (mUserBigramDictionary != null) {
- mUserBigramDictionary.getBigrams(wordComposer, prevWordForBigram, this,
- mNextLettersFrequencies);
+ for (final Dictionary dictionary : mBigramDictionaries.values()) {
+ dictionary.getBigrams(wordComposer, prevWordForBigram, this);
}
- if (mContactsDictionary != null) {
- mContactsDictionary.getBigrams(wordComposer, prevWordForBigram, this,
- mNextLettersFrequencies);
- }
- if (mMainDict != null) {
- mMainDict.getBigrams(wordComposer, prevWordForBigram, this,
- mNextLettersFrequencies);
- }
- char currentChar = wordComposer.getTypedWord().charAt(0);
- // TODO: Must pay attention to locale when changing case.
- char currentCharUpper = Character.toUpperCase(currentChar);
- int count = 0;
- int bigramSuggestionSize = mBigramSuggestions.size();
- for (int i = 0; i < bigramSuggestionSize; i++) {
- if (mBigramSuggestions.get(i).charAt(0) == currentChar
- || mBigramSuggestions.get(i).charAt(0) == currentCharUpper) {
- int poolSize = mStringPool.size();
- StringBuilder sb = poolSize > 0 ?
- (StringBuilder) mStringPool.remove(poolSize - 1)
- : new StringBuilder(getApproxMaxWordLength());
- sb.setLength(0);
- sb.append(mBigramSuggestions.get(i));
- mSuggestions.add(count++, sb);
- if (count > mPrefMaxSuggestions) break;
+ if (TextUtils.isEmpty(typedWord)) {
+ // Nothing entered: return all bigrams for the previous word
+ int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
+ for (int i = 0; i < insertCount; ++i) {
+ addBigramToSuggestions(mBigramSuggestions.get(i));
+ }
+ } else {
+ // Word entered: return only bigrams that match the first char of the typed word
+ final char currentChar = typedWord.charAt(0);
+ // TODO: Must pay attention to locale when changing case.
+ final char currentCharUpper = Character.toUpperCase(currentChar);
+ int count = 0;
+ final int bigramSuggestionSize = mBigramSuggestions.size();
+ for (int i = 0; i < bigramSuggestionSize; i++) {
+ final CharSequence bigramSuggestion = mBigramSuggestions.get(i);
+ final char bigramSuggestionFirstChar = bigramSuggestion.charAt(0);
+ if (bigramSuggestionFirstChar == currentChar
+ || bigramSuggestionFirstChar == currentCharUpper) {
+ addBigramToSuggestions(bigramSuggestion);
+ if (++count > mPrefMaxSuggestions) break;
+ }
}
}
}
} else if (wordComposer.size() > 1) {
// At second character typed, search the unigrams (scores being affected by bigrams)
- if (mUserDictionary != null || mContactsDictionary != null) {
- if (mUserDictionary != null) {
- mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
- }
- if (mContactsDictionary != null) {
- mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
- }
-
- if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)
- && (mCorrectionMode == CORRECTION_FULL
- || mCorrectionMode == CORRECTION_FULL_BIGRAM)) {
- mHaveCorrection = true;
- }
- }
- mMainDict.getWords(wordComposer, this, mNextLettersFrequencies);
- if ((mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM)
- && mSuggestions.size() > 0) {
- mHaveCorrection = true;
- }
- }
- if (mOriginalWord != null) {
- mSuggestions.add(0, mOriginalWord.toString());
- }
-
- // Check if the first suggestion has a minimum number of characters in common
- if (wordComposer.size() > 1 && mSuggestions.size() > 1
- && (mCorrectionMode == CORRECTION_FULL
- || mCorrectionMode == CORRECTION_FULL_BIGRAM)) {
- if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) {
- mHaveCorrection = false;
+ for (final String key : mUnigramDictionaries.keySet()) {
+ // Skip AutoDictionary and WhitelistDictionary to lookup
+ if (key.equals(DICT_KEY_AUTO) || key.equals(DICT_KEY_WHITELIST))
+ continue;
+ final Dictionary dictionary = mUnigramDictionaries.get(key);
+ dictionary.getWords(wordComposer, this);
}
}
- if (mAutoTextEnabled) {
- int i = 0;
- int max = 6;
- // Don't autotext the suggestions from the dictionaries
- if (mCorrectionMode == CORRECTION_BASIC) max = 1;
- while (i < mSuggestions.size() && i < max) {
- String suggestedWord = mSuggestions.get(i).toString().toLowerCase();
- CharSequence autoText =
- AutoText.get(suggestedWord, 0, suggestedWord.length(), view);
- // Is there an AutoText correction?
- boolean canAdd = autoText != null;
+ CharSequence autoText = null;
+ final String typedWordString = typedWord == null ? null : typedWord.toString();
+ if (typedWord != null) {
+ // Apply quick fix only for the typed word.
+ if (mQuickFixesEnabled) {
+ final String lowerCaseTypedWord = typedWordString.toLowerCase();
+ CharSequence tempAutoText = capitalizeWord(
+ mIsAllUpperCase, mIsFirstCharCapitalized, AutoText.get(
+ lowerCaseTypedWord, 0, lowerCaseTypedWord.length(), view));
+ // TODO: cleanup canAdd
+ // Is there an AutoText (also known as Quick Fixes) correction?
+ // Capitalize as needed
+ boolean canAdd = tempAutoText != null;
// Is that correction already the current prediction (or original word)?
- canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i));
+ canAdd &= !TextUtils.equals(tempAutoText, typedWord);
// Is that correction already the next predicted word?
- if (canAdd && i + 1 < mSuggestions.size() && mCorrectionMode != CORRECTION_BASIC) {
- canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1));
+ if (canAdd && mSuggestions.size() > 0 && mCorrectionMode != CORRECTION_BASIC) {
+ canAdd &= !TextUtils.equals(tempAutoText, mSuggestions.get(0));
}
if (canAdd) {
- mHaveCorrection = true;
- mSuggestions.add(i + 1, autoText);
- i++;
+ if (DBG) {
+ Log.d(TAG, "Auto corrected by AUTOTEXT.");
+ }
+ autoText = tempAutoText;
}
- i++;
}
}
+
+ CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase, mIsFirstCharCapitalized,
+ mWhiteListDictionary.getWhiteListedWord(typedWordString));
+
+ mAutoCorrection.updateAutoCorrectionStatus(mUnigramDictionaries, wordComposer,
+ mSuggestions, mScores, typedWord, mAutoCorrectionThreshold, mCorrectionMode,
+ autoText, whitelistedWord);
+
+ if (autoText != null) {
+ mSuggestions.add(0, autoText);
+ }
+
+ if (whitelistedWord != null) {
+ mSuggestions.add(0, whitelistedWord);
+ }
+
+ if (typedWord != null) {
+ mSuggestions.add(0, typedWordString);
+ }
removeDupes();
- return mSuggestions;
- }
- public int[] getNextLettersFrequencies() {
- return mNextLettersFrequencies;
+ if (DBG) {
+ double normalizedScore = mAutoCorrection.getNormalizedScore();
+ ArrayList<SuggestedWords.SuggestedWordInfo> scoreInfoList =
+ new ArrayList<SuggestedWords.SuggestedWordInfo>();
+ scoreInfoList.add(new SuggestedWords.SuggestedWordInfo("+", false));
+ for (int i = 0; i < mScores.length; ++i) {
+ if (normalizedScore > 0) {
+ final String scoreThreshold = String.format("%d (%4.2f)", mScores[i],
+ normalizedScore);
+ scoreInfoList.add(
+ new SuggestedWords.SuggestedWordInfo(scoreThreshold, false));
+ normalizedScore = 0.0;
+ } else {
+ final String score = Integer.toString(mScores[i]);
+ scoreInfoList.add(new SuggestedWords.SuggestedWordInfo(score, false));
+ }
+ }
+ for (int i = mScores.length; i < mSuggestions.size(); ++i) {
+ scoreInfoList.add(new SuggestedWords.SuggestedWordInfo("--", false));
+ }
+ return new SuggestedWords.Builder().addWords(mSuggestions, scoreInfoList);
+ }
+ return new SuggestedWords.Builder().addWords(mSuggestions, null);
}
private void removeDupes() {
@@ -378,45 +442,44 @@ public class Suggest implements Dictionary.WordCallback {
}
}
- public boolean hasMinimalCorrection() {
- return mHaveCorrection;
- }
-
- private boolean compareCaseInsensitive(final String mLowerOriginalWord,
- final char[] word, final int offset, final int length) {
- final int originalLength = mLowerOriginalWord.length();
- if (originalLength == length && Character.isUpperCase(word[offset])) {
- for (int i = 0; i < originalLength; i++) {
- if (mLowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) {
- return false;
- }
- }
- return true;
- }
- return false;
+ public boolean hasAutoCorrection() {
+ return mAutoCorrection.hasAutoCorrection();
}
- public boolean addWord(final char[] word, final int offset, final int length, int freq,
+ @Override
+ public boolean addWord(final char[] word, final int offset, final int length, int score,
final int dicTypeId, final Dictionary.DataType dataType) {
Dictionary.DataType dataTypeForLog = dataType;
- ArrayList<CharSequence> suggestions;
- int[] priorities;
- int prefMaxSuggestions;
+ final ArrayList<CharSequence> suggestions;
+ final int[] sortedScores;
+ final int prefMaxSuggestions;
if(dataType == Dictionary.DataType.BIGRAM) {
suggestions = mBigramSuggestions;
- priorities = mBigramPriorities;
+ sortedScores = mBigramScores;
prefMaxSuggestions = PREF_MAX_BIGRAMS;
} else {
suggestions = mSuggestions;
- priorities = mPriorities;
+ sortedScores = mScores;
prefMaxSuggestions = mPrefMaxSuggestions;
}
int pos = 0;
// Check if it's the same word, only caps are different
- if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) {
- pos = 0;
+ if (Utils.equalsIgnoreCase(mTypedWord, word, offset, length)) {
+ // TODO: remove this surrounding if clause and move this logic to
+ // getSuggestedWordBuilder.
+ if (suggestions.size() > 0) {
+ final String currentHighestWord = suggestions.get(0).toString();
+ // If the current highest word is also equal to typed word, we need to compare
+ // frequency to determine the insertion position. This does not ensure strictly
+ // correct ordering, but ensures the top score is on top which is enough for
+ // removing duplicates correctly.
+ if (Utils.equalsIgnoreCase(currentHighestWord, word, offset, length)
+ && score <= sortedScores[0]) {
+ pos = 1;
+ }
+ }
} else {
if (dataType == Dictionary.DataType.UNIGRAM) {
// Check if the word was already added before (by bigram data)
@@ -424,24 +487,24 @@ public class Suggest implements Dictionary.WordCallback {
if(bigramSuggestion >= 0) {
dataTypeForLog = Dictionary.DataType.BIGRAM;
// turn freq from bigram into multiplier specified above
- double multiplier = (((double) mBigramPriorities[bigramSuggestion])
+ double multiplier = (((double) mBigramScores[bigramSuggestion])
/ MAXIMUM_BIGRAM_FREQUENCY)
* (BIGRAM_MULTIPLIER_MAX - BIGRAM_MULTIPLIER_MIN)
+ BIGRAM_MULTIPLIER_MIN;
/* Log.d(TAG,"bigram num: " + bigramSuggestion
+ " wordB: " + mBigramSuggestions.get(bigramSuggestion).toString()
- + " currentPriority: " + freq + " bigramPriority: "
- + mBigramPriorities[bigramSuggestion]
+ + " currentScore: " + score + " bigramScore: "
+ + mBigramScores[bigramSuggestion]
+ " multiplier: " + multiplier); */
- freq = (int)Math.round((freq * multiplier));
+ score = (int)Math.round((score * multiplier));
}
}
- // Check the last one's priority and bail
- if (priorities[prefMaxSuggestions - 1] >= freq) return true;
+ // Check the last one's score and bail
+ if (sortedScores[prefMaxSuggestions - 1] >= score) return true;
while (pos < prefMaxSuggestions) {
- if (priorities[pos] < freq
- || (priorities[pos] == freq && length < suggestions.get(pos).length())) {
+ if (sortedScores[pos] < score
+ || (sortedScores[pos] == score && length < suggestions.get(pos).length())) {
break;
}
pos++;
@@ -451,11 +514,10 @@ public class Suggest implements Dictionary.WordCallback {
return true;
}
- System.arraycopy(priorities, pos, priorities, pos + 1,
- prefMaxSuggestions - pos - 1);
- priorities[pos] = freq;
+ System.arraycopy(sortedScores, pos, sortedScores, pos + 1, prefMaxSuggestions - pos - 1);
+ sortedScores[pos] = score;
int poolSize = mStringPool.size();
- StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
+ StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
: new StringBuilder(getApproxMaxWordLength());
sb.setLength(0);
// TODO: Must pay attention to locale when changing case.
@@ -501,16 +563,6 @@ public class Suggest implements Dictionary.WordCallback {
return -1;
}
- public boolean isValidWord(final CharSequence word) {
- if (word == null || word.length() == 0) {
- return false;
- }
- return mMainDict.isValidWord(word)
- || (mUserDictionary != null && mUserDictionary.isValidWord(word))
- || (mAutoDictionary != null && mAutoDictionary.isValidWord(word))
- || (mContactsDictionary != null && mContactsDictionary.isValidWord(word));
- }
-
private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) {
int poolSize = mStringPool.size();
int garbageSize = suggestions.size();
@@ -529,8 +581,12 @@ public class Suggest implements Dictionary.WordCallback {
}
public void close() {
- if (mMainDict != null) {
- mMainDict.close();
+ final Set<Dictionary> dictionaries = new HashSet<Dictionary>();
+ dictionaries.addAll(mUnigramDictionaries.values());
+ dictionaries.addAll(mBigramDictionaries.values());
+ for (final Dictionary dictionary : dictionaries) {
+ dictionary.close();
}
+ mMainDict = null;
}
}
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
new file mode 100644
index 000000000..a8cdfc02e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -0,0 +1,184 @@
+/*
+ * 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.latin;
+
+import android.view.inputmethod.CompletionInfo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+public class SuggestedWords {
+ public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, null);
+
+ public final List<CharSequence> mWords;
+ public final boolean mTypedWordValid;
+ public final boolean mHasMinimalSuggestion;
+ public final List<SuggestedWordInfo> mSuggestedWordInfoList;
+
+ private SuggestedWords(List<CharSequence> words, boolean typedWordValid,
+ boolean hasMinimalSuggestion, List<SuggestedWordInfo> suggestedWordInfoList) {
+ if (words != null) {
+ mWords = words;
+ } else {
+ mWords = Collections.emptyList();
+ }
+ mTypedWordValid = typedWordValid;
+ mHasMinimalSuggestion = hasMinimalSuggestion;
+ mSuggestedWordInfoList = suggestedWordInfoList;
+ }
+
+ public int size() {
+ return mWords.size();
+ }
+
+ public CharSequence getWord(int pos) {
+ return mWords.get(pos);
+ }
+
+ public boolean hasAutoCorrectionWord() {
+ return mHasMinimalSuggestion && size() > 1 && !mTypedWordValid;
+ }
+
+ public boolean hasWordAboveAutoCorrectionScoreThreshold() {
+ return mHasMinimalSuggestion && ((size() > 1 && !mTypedWordValid) || mTypedWordValid);
+ }
+
+ public static class Builder {
+ private List<CharSequence> mWords = new ArrayList<CharSequence>();
+ private boolean mTypedWordValid;
+ private boolean mHasMinimalSuggestion;
+ private List<SuggestedWordInfo> mSuggestedWordInfoList =
+ new ArrayList<SuggestedWordInfo>();
+
+ public Builder() {
+ // Nothing to do here.
+ }
+
+ public Builder addWords(List<CharSequence> words,
+ List<SuggestedWordInfo> suggestedWordInfoList) {
+ final int N = words.size();
+ for (int i = 0; i < N; ++i) {
+ SuggestedWordInfo suggestedWordInfo = null;
+ if (suggestedWordInfoList != null) {
+ suggestedWordInfo = suggestedWordInfoList.get(i);
+ }
+ if (suggestedWordInfo == null) {
+ suggestedWordInfo = new SuggestedWordInfo();
+ }
+ addWord(words.get(i), suggestedWordInfo);
+ }
+ return this;
+ }
+
+ public Builder addWord(CharSequence word) {
+ return addWord(word, null, false);
+ }
+
+ public Builder addWord(CharSequence word, CharSequence debugString,
+ boolean isPreviousSuggestedWord) {
+ SuggestedWordInfo info = new SuggestedWordInfo(debugString, isPreviousSuggestedWord);
+ return addWord(word, info);
+ }
+
+ private Builder addWord(CharSequence word, SuggestedWordInfo suggestedWordInfo) {
+ mWords.add(word);
+ mSuggestedWordInfoList.add(suggestedWordInfo);
+ return this;
+ }
+
+ public Builder setApplicationSpecifiedCompletions(CompletionInfo[] infos) {
+ for (CompletionInfo info : infos)
+ addWord(info.getText());
+ return this;
+ }
+
+ public Builder setTypedWordValid(boolean typedWordValid) {
+ mTypedWordValid = typedWordValid;
+ return this;
+ }
+
+ public Builder setHasMinimalSuggestion(boolean hasMinimalSuggestion) {
+ mHasMinimalSuggestion = hasMinimalSuggestion;
+ return this;
+ }
+
+ // Should get rid of the first one (what the user typed previously) from suggestions
+ // and replace it with what the user currently typed.
+ public Builder addTypedWordAndPreviousSuggestions(CharSequence typedWord,
+ SuggestedWords previousSuggestions) {
+ mWords.clear();
+ mSuggestedWordInfoList.clear();
+ final HashSet<String> alreadySeen = new HashSet<String>();
+ addWord(typedWord, null, false);
+ alreadySeen.add(typedWord.toString());
+ final int previousSize = previousSuggestions.size();
+ for (int pos = 1; pos < previousSize; pos++) {
+ final String prevWord = previousSuggestions.getWord(pos).toString();
+ // Filter out duplicate suggestion.
+ if (!alreadySeen.contains(prevWord)) {
+ addWord(prevWord, null, true);
+ alreadySeen.add(prevWord);
+ }
+ }
+ mTypedWordValid = false;
+ mHasMinimalSuggestion = false;
+ return this;
+ }
+
+ public SuggestedWords build() {
+ return new SuggestedWords(mWords, mTypedWordValid, mHasMinimalSuggestion,
+ mSuggestedWordInfoList);
+ }
+
+ public int size() {
+ return mWords.size();
+ }
+
+ public CharSequence getWord(int pos) {
+ return mWords.get(pos);
+ }
+ }
+
+ public static class SuggestedWordInfo {
+ private final CharSequence mDebugString;
+ private final boolean mPreviousSuggestedWord;
+
+ public SuggestedWordInfo() {
+ mDebugString = "";
+ mPreviousSuggestedWord = false;
+ }
+
+ public SuggestedWordInfo(CharSequence debugString, boolean previousSuggestedWord) {
+ mDebugString = debugString;
+ mPreviousSuggestedWord = previousSuggestedWord;
+ }
+
+ public String getDebugString() {
+ if (mDebugString == null) {
+ return "";
+ } else {
+ return mDebugString.toString();
+ }
+ }
+
+ public boolean isPreviousSuggestedWord () {
+ return mPreviousSuggestedWord;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java b/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java
new file mode 100644
index 000000000..4a3f42d5d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java
@@ -0,0 +1,43 @@
+/*
+ * 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.latin;
+
+import com.android.inputmethod.compat.SuggestionSpanUtils;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class SuggestionSpanPickedNotificationReceiver extends BroadcastReceiver {
+ private static final boolean DBG = LatinImeLogger.sDBG;
+ private static final String TAG =
+ SuggestionSpanPickedNotificationReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (SuggestionSpanUtils.ACTION_SUGGESTION_PICKED.equals(intent.getAction())) {
+ if (DBG) {
+ final String before = intent.getStringExtra(
+ SuggestionSpanUtils.SUGGESTION_SPAN_PICKED_BEFORE);
+ final String after = intent.getStringExtra(
+ SuggestionSpanUtils.SUGGESTION_SPAN_PICKED_AFTER);
+ Log.d(TAG, "Received notification picked: " + before + "," + after);
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java
index 9011191f1..de13f3ae4 100644
--- a/java/src/com/android/inputmethod/latin/TextEntryState.java
+++ b/java/src/com/android/inputmethod/latin/TextEntryState.java
@@ -16,116 +16,43 @@
package com.android.inputmethod.latin;
-import android.content.Context;
-import android.inputmethodservice.Keyboard.Key;
-import android.text.format.DateFormat;
-import android.util.Log;
+import com.android.inputmethod.latin.Utils.RingCharBuffer;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Calendar;
+import android.util.Log;
public class TextEntryState {
-
- private static final boolean DBG = false;
-
- private static final String TAG = "TextEntryState";
-
- private static boolean LOGGING = false;
-
- private static int sBackspaceCount = 0;
-
- private static int sAutoSuggestCount = 0;
-
- private static int sAutoSuggestUndoneCount = 0;
-
- private static int sManualSuggestCount = 0;
-
- private static int sWordNotInDictionaryCount = 0;
-
- private static int sSessionCount = 0;
-
- private static int sTypedChars;
-
- private static int sActualChars;
-
- public enum State {
- UNKNOWN,
- START,
- IN_WORD,
- ACCEPTED_DEFAULT,
- PICKED_SUGGESTION,
- PUNCTUATION_AFTER_WORD,
- PUNCTUATION_AFTER_ACCEPTED,
- SPACE_AFTER_ACCEPTED,
- SPACE_AFTER_PICKED,
- UNDO_COMMIT,
- CORRECTING,
- PICKED_CORRECTION;
+ private static final String TAG = TextEntryState.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final int UNKNOWN = 0;
+ private static final int START = 1;
+ private static final int IN_WORD = 2;
+ private static final int ACCEPTED_DEFAULT = 3;
+ private static final int PICKED_SUGGESTION = 4;
+ private static final int PUNCTUATION_AFTER_WORD = 5;
+ private static final int PUNCTUATION_AFTER_ACCEPTED = 6;
+ private static final int SPACE_AFTER_ACCEPTED = 7;
+ private static final int SPACE_AFTER_PICKED = 8;
+ private static final int UNDO_COMMIT = 9;
+ private static final int RECORRECTING = 10;
+ private static final int PICKED_RECORRECTION = 11;
+
+ private static int sState = UNKNOWN;
+ private static int sPreviousState = UNKNOWN;
+
+ private static void setState(final int newState) {
+ sPreviousState = sState;
+ sState = newState;
}
- private static State sState = State.UNKNOWN;
-
- private static FileOutputStream sKeyLocationFile;
- private static FileOutputStream sUserActionFile;
-
- public static void newSession(Context context) {
- sSessionCount++;
- sAutoSuggestCount = 0;
- sBackspaceCount = 0;
- sAutoSuggestUndoneCount = 0;
- sManualSuggestCount = 0;
- sWordNotInDictionaryCount = 0;
- sTypedChars = 0;
- sActualChars = 0;
- sState = State.START;
-
- if (LOGGING) {
- try {
- sKeyLocationFile = context.openFileOutput("key.txt", Context.MODE_APPEND);
- sUserActionFile = context.openFileOutput("action.txt", Context.MODE_APPEND);
- } catch (IOException ioe) {
- Log.e("TextEntryState", "Couldn't open file for output: " + ioe);
- }
- }
- }
-
- public static void endSession() {
- if (sKeyLocationFile == null) {
- return;
- }
- try {
- sKeyLocationFile.close();
- // Write to log file
- // Write timestamp, settings,
- String out = DateFormat.format("MM:dd hh:mm:ss", Calendar.getInstance().getTime())
- .toString()
- + " BS: " + sBackspaceCount
- + " auto: " + sAutoSuggestCount
- + " manual: " + sManualSuggestCount
- + " typed: " + sWordNotInDictionaryCount
- + " undone: " + sAutoSuggestUndoneCount
- + " saved: " + ((float) (sActualChars - sTypedChars) / sActualChars)
- + "\n";
- sUserActionFile.write(out.getBytes());
- sUserActionFile.close();
- sKeyLocationFile = null;
- sUserActionFile = null;
- } catch (IOException ioe) {
-
- }
- }
-
- public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) {
+ public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord,
+ int separatorCode) {
if (typedWord == null) return;
- if (!typedWord.equals(actualWord)) {
- sAutoSuggestCount++;
- }
- sTypedChars += typedWord.length();
- sActualChars += actualWord.length();
- sState = State.ACCEPTED_DEFAULT;
- LatinImeLogger.logOnAutoSuggestion(typedWord.toString(), actualWord.toString());
- displayState();
+ setState(ACCEPTED_DEFAULT);
+ LatinImeLogger.logOnAutoCorrection(
+ typedWord.toString(), actualWord.toString(), separatorCode);
+ if (DEBUG)
+ displayState("acceptedDefault", "typedWord", typedWord, "actualWord", actualWord);
}
// State.ACCEPTED_DEFAULT will be changed to other sub-states
@@ -134,145 +61,176 @@ public class TextEntryState {
public static void backToAcceptedDefault(CharSequence typedWord) {
if (typedWord == null) return;
switch (sState) {
- case SPACE_AFTER_ACCEPTED:
- case PUNCTUATION_AFTER_ACCEPTED:
- case IN_WORD:
- sState = State.ACCEPTED_DEFAULT;
- break;
+ case SPACE_AFTER_ACCEPTED:
+ case PUNCTUATION_AFTER_ACCEPTED:
+ case IN_WORD:
+ setState(ACCEPTED_DEFAULT);
+ break;
+ default:
+ break;
}
- displayState();
+ if (DEBUG) displayState("backToAcceptedDefault", "typedWord", typedWord);
}
public static void acceptedTyped(CharSequence typedWord) {
- sWordNotInDictionaryCount++;
- sState = State.PICKED_SUGGESTION;
- displayState();
+ setState(PICKED_SUGGESTION);
+ if (DEBUG) displayState("acceptedTyped", "typedWord", typedWord);
}
public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) {
- sManualSuggestCount++;
- State oldState = sState;
- if (typedWord.equals(actualWord)) {
- acceptedTyped(typedWord);
- }
- if (oldState == State.CORRECTING || oldState == State.PICKED_CORRECTION) {
- sState = State.PICKED_CORRECTION;
+ if (sState == RECORRECTING || sState == PICKED_RECORRECTION) {
+ setState(PICKED_RECORRECTION);
} else {
- sState = State.PICKED_SUGGESTION;
+ setState(PICKED_SUGGESTION);
}
- displayState();
+ if (DEBUG)
+ displayState("acceptedSuggestion", "typedWord", typedWord, "actualWord", actualWord);
+ }
+
+ public static void selectedForRecorrection() {
+ setState(RECORRECTING);
+ if (DEBUG) displayState("selectedForRecorrection");
}
- public static void selectedForCorrection() {
- sState = State.CORRECTING;
- displayState();
+ public static void onAbortRecorrection() {
+ if (sState == RECORRECTING || sState == PICKED_RECORRECTION) {
+ setState(START);
+ }
+ if (DEBUG) displayState("onAbortRecorrection");
}
- public static void typedCharacter(char c, boolean isSeparator) {
- boolean isSpace = c == ' ';
+ public static void typedCharacter(char c, boolean isSeparator, int x, int y) {
+ final boolean isSpace = (c == ' ');
switch (sState) {
- case IN_WORD:
- if (isSpace || isSeparator) {
- sState = State.START;
- } else {
- // State hasn't changed.
- }
- break;
- case ACCEPTED_DEFAULT:
- case SPACE_AFTER_PICKED:
- if (isSpace) {
- sState = State.SPACE_AFTER_ACCEPTED;
- } else if (isSeparator) {
- sState = State.PUNCTUATION_AFTER_ACCEPTED;
- } else {
- sState = State.IN_WORD;
- }
- break;
- case PICKED_SUGGESTION:
- case PICKED_CORRECTION:
- if (isSpace) {
- sState = State.SPACE_AFTER_PICKED;
- } else if (isSeparator) {
- // Swap
- sState = State.PUNCTUATION_AFTER_ACCEPTED;
- } else {
- sState = State.IN_WORD;
- }
- break;
- case START:
- case UNKNOWN:
- case SPACE_AFTER_ACCEPTED:
- case PUNCTUATION_AFTER_ACCEPTED:
- case PUNCTUATION_AFTER_WORD:
- if (!isSpace && !isSeparator) {
- sState = State.IN_WORD;
- } else {
- sState = State.START;
- }
- break;
- case UNDO_COMMIT:
- if (isSpace || isSeparator) {
- sState = State.ACCEPTED_DEFAULT;
- } else {
- sState = State.IN_WORD;
- }
- break;
- case CORRECTING:
- sState = State.START;
- break;
+ case IN_WORD:
+ if (isSpace || isSeparator) {
+ setState(START);
+ } else {
+ // State hasn't changed.
+ }
+ break;
+ case ACCEPTED_DEFAULT:
+ case SPACE_AFTER_PICKED:
+ case PUNCTUATION_AFTER_ACCEPTED:
+ if (isSpace) {
+ setState(SPACE_AFTER_ACCEPTED);
+ } else if (isSeparator) {
+ // Swap
+ setState(PUNCTUATION_AFTER_ACCEPTED);
+ } else {
+ setState(IN_WORD);
+ }
+ break;
+ case PICKED_SUGGESTION:
+ case PICKED_RECORRECTION:
+ if (isSpace) {
+ setState(SPACE_AFTER_PICKED);
+ } else if (isSeparator) {
+ // Swap
+ setState(PUNCTUATION_AFTER_ACCEPTED);
+ } else {
+ setState(IN_WORD);
+ }
+ break;
+ case START:
+ case UNKNOWN:
+ case SPACE_AFTER_ACCEPTED:
+ case PUNCTUATION_AFTER_WORD:
+ if (!isSpace && !isSeparator) {
+ setState(IN_WORD);
+ } else {
+ setState(START);
+ }
+ break;
+ case UNDO_COMMIT:
+ if (isSpace || isSeparator) {
+ setState(ACCEPTED_DEFAULT);
+ } else {
+ setState(IN_WORD);
+ }
+ break;
+ case RECORRECTING:
+ setState(START);
+ break;
+ }
+ RingCharBuffer.getInstance().push(c, x, y);
+ if (isSeparator) {
+ LatinImeLogger.logOnInputSeparator();
+ } else {
+ LatinImeLogger.logOnInputChar();
}
- displayState();
+ if (DEBUG) displayState("typedCharacter", "char", c, "isSeparator", isSeparator);
}
public static void backspace() {
- if (sState == State.ACCEPTED_DEFAULT) {
- sState = State.UNDO_COMMIT;
- sAutoSuggestUndoneCount++;
- LatinImeLogger.logOnAutoSuggestionCanceled();
- } else if (sState == State.UNDO_COMMIT) {
- sState = State.IN_WORD;
+ if (sState == ACCEPTED_DEFAULT) {
+ setState(UNDO_COMMIT);
+ LatinImeLogger.logOnAutoCorrectionCancelled();
+ } else if (sState == UNDO_COMMIT) {
+ setState(IN_WORD);
}
- sBackspaceCount++;
- displayState();
+ if (DEBUG) displayState("backspace");
}
public static void reset() {
- sState = State.START;
- displayState();
+ setState(START);
+ if (DEBUG) displayState("reset");
}
- public static State getState() {
- if (DBG) {
- Log.d(TAG, "Returning state = " + sState);
- }
- return sState;
+ public static boolean isAcceptedDefault() {
+ return sState == ACCEPTED_DEFAULT;
}
- public static boolean isCorrecting() {
- return sState == State.CORRECTING || sState == State.PICKED_CORRECTION;
+ public static boolean isSpaceAfterPicked() {
+ return sState == SPACE_AFTER_PICKED;
}
- public static void keyPressedAt(Key key, int x, int y) {
- if (LOGGING && sKeyLocationFile != null && key.codes[0] >= 32) {
- String out =
- "KEY: " + (char) key.codes[0]
- + " X: " + x
- + " Y: " + y
- + " MX: " + (key.x + key.width / 2)
- + " MY: " + (key.y + key.height / 2)
- + "\n";
- try {
- sKeyLocationFile.write(out.getBytes());
- } catch (IOException ioe) {
- // TODO: May run out of space
- }
+ public static boolean isUndoCommit() {
+ return sState == UNDO_COMMIT;
+ }
+
+ public static boolean isPunctuationAfterAccepted() {
+ return sState == PUNCTUATION_AFTER_ACCEPTED;
+ }
+
+ public static boolean isRecorrecting() {
+ return sState == RECORRECTING || sState == PICKED_RECORRECTION;
+ }
+
+ public static String getState() {
+ return stateName(sState);
+ }
+
+ private static String stateName(int state) {
+ switch (state) {
+ case START: return "START";
+ case IN_WORD: return "IN_WORD";
+ case ACCEPTED_DEFAULT: return "ACCEPTED_DEFAULT";
+ case PICKED_SUGGESTION: return "PICKED_SUGGESTION";
+ case PUNCTUATION_AFTER_WORD: return "PUNCTUATION_AFTER_WORD";
+ case PUNCTUATION_AFTER_ACCEPTED: return "PUNCTUATION_AFTER_ACCEPTED";
+ case SPACE_AFTER_ACCEPTED: return "SPACE_AFTER_ACCEPTED";
+ case SPACE_AFTER_PICKED: return "SPACE_AFTER_PICKED";
+ case UNDO_COMMIT: return "UNDO_COMMIT";
+ case RECORRECTING: return "RECORRECTING";
+ case PICKED_RECORRECTION: return "PICKED_RECORRECTION";
+ default: return "UNKNOWN";
}
}
- private static void displayState() {
- if (DBG) {
- Log.d(TAG, "State = " + sState);
+ private static void displayState(String title, Object ... args) {
+ final StringBuilder sb = new StringBuilder(title);
+ sb.append(':');
+ for (int i = 0; i < args.length; i += 2) {
+ sb.append(' ');
+ sb.append(args[i]);
+ sb.append('=');
+ sb.append(args[i+1].toString());
}
+ sb.append(" state=");
+ sb.append(stateName(sState));
+ sb.append(" previous=");
+ sb.append(stateName(sPreviousState));
+ Log.d(TAG, sb.toString());
}
}
-
diff --git a/java/src/com/android/inputmethod/latin/Tutorial.java b/java/src/com/android/inputmethod/latin/Tutorial.java
deleted file mode 100644
index d3eaf30c6..000000000
--- a/java/src/com/android/inputmethod/latin/Tutorial.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * 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.latin;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.os.Message;
-import android.text.Layout;
-import android.text.SpannableStringBuilder;
-import android.text.StaticLayout;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.View.OnTouchListener;
-import android.widget.PopupWindow;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class Tutorial implements OnTouchListener {
-
- private List<Bubble> mBubbles = new ArrayList<Bubble>();
- private View mInputView;
- private LatinIME mIme;
- private int[] mLocation = new int[2];
-
- private static final int MSG_SHOW_BUBBLE = 0;
-
- private int mBubbleIndex;
-
- Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_SHOW_BUBBLE:
- Bubble bubba = (Bubble) msg.obj;
- bubba.show(mLocation[0], mLocation[1]);
- break;
- }
- }
- };
-
- class Bubble {
- Drawable bubbleBackground;
- int x;
- int y;
- int width;
- int gravity;
- CharSequence text;
- boolean dismissOnTouch;
- boolean dismissOnClose;
- PopupWindow window;
- TextView textView;
- View inputView;
-
- Bubble(Context context, View inputView,
- int backgroundResource, int bx, int by, int textResource1, int textResource2) {
- bubbleBackground = context.getResources().getDrawable(backgroundResource);
- x = bx;
- y = by;
- width = (int) (inputView.getWidth() * 0.9);
- this.gravity = Gravity.TOP | Gravity.LEFT;
- text = new SpannableStringBuilder()
- .append(context.getResources().getText(textResource1))
- .append("\n")
- .append(context.getResources().getText(textResource2));
- this.dismissOnTouch = true;
- this.dismissOnClose = false;
- this.inputView = inputView;
- window = new PopupWindow(context);
- window.setBackgroundDrawable(null);
- LayoutInflater inflate =
- (LayoutInflater) context
- .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- textView = (TextView) inflate.inflate(R.layout.bubble_text, null);
- textView.setBackgroundDrawable(bubbleBackground);
- textView.setText(text);
- //textView.setText(textResource1);
- window.setContentView(textView);
- window.setFocusable(false);
- window.setTouchable(true);
- window.setOutsideTouchable(false);
- }
-
- private int chooseSize(PopupWindow pop, View parentView, CharSequence text, TextView tv) {
- int wid = tv.getPaddingLeft() + tv.getPaddingRight();
- int ht = tv.getPaddingTop() + tv.getPaddingBottom();
-
- /*
- * Figure out how big the text would be if we laid it out to the
- * full width of this view minus the border.
- */
- int cap = width - wid;
-
- Layout l = new StaticLayout(text, tv.getPaint(), cap,
- Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
- float max = 0;
- for (int i = 0; i < l.getLineCount(); i++) {
- max = Math.max(max, l.getLineWidth(i));
- }
-
- /*
- * Now set the popup size to be big enough for the text plus the border.
- */
- pop.setWidth(width);
- pop.setHeight(ht + l.getHeight());
- return l.getHeight();
- }
-
- void show(int offx, int offy) {
- int textHeight = chooseSize(window, inputView, text, textView);
- offy -= textView.getPaddingTop() + textHeight;
- if (inputView.getVisibility() == View.VISIBLE
- && inputView.getWindowVisibility() == View.VISIBLE) {
- try {
- if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) offy -= window.getHeight();
- if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) offx -= window.getWidth();
- textView.setOnTouchListener(new View.OnTouchListener() {
- public boolean onTouch(View view, MotionEvent me) {
- Tutorial.this.next();
- return true;
- }
- });
- window.showAtLocation(inputView, Gravity.NO_GRAVITY, x + offx, y + offy);
- } catch (Exception e) {
- // Input view is not valid
- }
- }
- }
-
- void hide() {
- if (window.isShowing()) {
- textView.setOnTouchListener(null);
- window.dismiss();
- }
- }
-
- boolean isShowing() {
- return window.isShowing();
- }
- }
-
- public Tutorial(LatinIME ime, LatinKeyboardView inputView) {
- Context context = inputView.getContext();
- mIme = ime;
- int inputWidth = inputView.getWidth();
- final int x = inputWidth / 20; // Half of 1/10th
- Bubble bWelcome = new Bubble(context, inputView,
- R.drawable.dialog_bubble_step02, x, 0,
- R.string.tip_to_open_keyboard, R.string.touch_to_continue);
- mBubbles.add(bWelcome);
- Bubble bAccents = new Bubble(context, inputView,
- R.drawable.dialog_bubble_step02, x, 0,
- R.string.tip_to_view_accents, R.string.touch_to_continue);
- mBubbles.add(bAccents);
- Bubble b123 = new Bubble(context, inputView,
- R.drawable.dialog_bubble_step07, x, 0,
- R.string.tip_to_open_symbols, R.string.touch_to_continue);
- mBubbles.add(b123);
- Bubble bABC = new Bubble(context, inputView,
- R.drawable.dialog_bubble_step07, x, 0,
- R.string.tip_to_close_symbols, R.string.touch_to_continue);
- mBubbles.add(bABC);
- Bubble bSettings = new Bubble(context, inputView,
- R.drawable.dialog_bubble_step07, x, 0,
- R.string.tip_to_launch_settings, R.string.touch_to_continue);
- mBubbles.add(bSettings);
- Bubble bDone = new Bubble(context, inputView,
- R.drawable.dialog_bubble_step02, x, 0,
- R.string.tip_to_start_typing, R.string.touch_to_finish);
- mBubbles.add(bDone);
- mInputView = inputView;
- }
-
- void start() {
- mInputView.getLocationInWindow(mLocation);
- mBubbleIndex = -1;
- mInputView.setOnTouchListener(this);
- next();
- }
-
- boolean next() {
- if (mBubbleIndex >= 0) {
- // If the bubble is not yet showing, don't move to the next.
- if (!mBubbles.get(mBubbleIndex).isShowing()) {
- return true;
- }
- // Hide all previous bubbles as well, as they may have had a delayed show
- for (int i = 0; i <= mBubbleIndex; i++) {
- mBubbles.get(i).hide();
- }
- }
- mBubbleIndex++;
- if (mBubbleIndex >= mBubbles.size()) {
- mInputView.setOnTouchListener(null);
- mIme.sendDownUpKeyEvents(-1); // Inform the setupwizard that tutorial is in last bubble
- mIme.tutorialDone();
- return false;
- }
- if (mBubbleIndex == 3 || mBubbleIndex == 4) {
- mIme.mKeyboardSwitcher.toggleSymbols();
- }
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_SHOW_BUBBLE, mBubbles.get(mBubbleIndex)), 500);
- return true;
- }
-
- void hide() {
- for (int i = 0; i < mBubbles.size(); i++) {
- mBubbles.get(i).hide();
- }
- mInputView.setOnTouchListener(null);
- }
-
- boolean close() {
- mHandler.removeMessages(MSG_SHOW_BUBBLE);
- hide();
- return true;
- }
-
- public boolean onTouch(View v, MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- next();
- }
- return true;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java
index 67d9c0bcf..5b615ca29 100644
--- a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 Google Inc.
+ * 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
@@ -16,10 +16,6 @@
package com.android.inputmethod.latin;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
@@ -30,6 +26,10 @@ import android.os.AsyncTask;
import android.provider.BaseColumns;
import android.util.Log;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+
/**
* Stores all the pairs user types in databases. Prune the database if the size
* gets too big. Unlike AutoDictionary, it even stores the pairs that are already
@@ -44,12 +44,6 @@ public class UserBigramDictionary extends ExpandableDictionary {
/** Maximum frequency for all pairs */
private static final int FREQUENCY_MAX = 127;
- /**
- * If this pair is typed 6 times, it would be suggested.
- * Should be smaller than ContactsDictionary.FREQUENCY_FOR_CONTACTS_BIGRAM
- */
- protected static final int SUGGEST_THRESHOLD = 6 * FREQUENCY_FOR_TYPED;
-
/** Maximum number of pairs. Pruning will start when databases goes above this number. */
private static int sMaxUserBigrams = 10000;
@@ -108,25 +102,25 @@ public class UserBigramDictionary extends ExpandableDictionary {
private static DatabaseHelper sOpenHelper = null;
private static class Bigram {
- String word1;
- String word2;
- int frequency;
+ public final String mWord1;
+ public final String mWord2;
+ public final int frequency;
Bigram(String word1, String word2, int frequency) {
- this.word1 = word1;
- this.word2 = word2;
+ this.mWord1 = word1;
+ this.mWord2 = word2;
this.frequency = frequency;
}
@Override
public boolean equals(Object bigram) {
Bigram bigram2 = (Bigram) bigram;
- return (word1.equals(bigram2.word1) && word2.equals(bigram2.word2));
+ return (mWord1.equals(bigram2.mWord1) && mWord2.equals(bigram2.mWord2));
}
@Override
public int hashCode() {
- return (word1 + " " + word2).hashCode();
+ return (mWord1 + " " + mWord2).hashCode();
}
}
@@ -164,10 +158,14 @@ public class UserBigramDictionary extends ExpandableDictionary {
* Pair will be added to the userbigram database.
*/
public int addBigrams(String word1, String word2) {
- // remove caps
+ // remove caps if second word is autocapitalized
if (mIme != null && mIme.getCurrentWord().isAutoCapitalized()) {
word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1);
}
+ // Do not insert a word as a bigram of itself
+ if (word1.equals(word2)) {
+ return 0;
+ }
int freq = super.addBigram(word1, word2, FREQUENCY_FOR_TYPED);
if (freq > FREQUENCY_MAX) freq = FREQUENCY_MAX;
@@ -357,7 +355,7 @@ public class UserBigramDictionary extends ExpandableDictionary {
Cursor c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID },
MAIN_COLUMN_WORD1 + "=? AND " + MAIN_COLUMN_WORD2 + "=? AND "
+ MAIN_COLUMN_LOCALE + "=?",
- new String[] { bi.word1, bi.word2, mLocale }, null, null, null);
+ new String[] { bi.mWord1, bi.mWord2, mLocale }, null, null, null);
int pairId;
if (c.moveToFirst()) {
@@ -368,7 +366,7 @@ public class UserBigramDictionary extends ExpandableDictionary {
} else {
// new pair
Long pairIdLong = db.insert(MAIN_TABLE_NAME, null,
- getContentValues(bi.word1, bi.word2, mLocale));
+ getContentValues(bi.mWord1, bi.mWord2, mLocale));
pairId = pairIdLong.intValue();
}
c.close();
diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java
index 49b95e9aa..c06bd736e 100644
--- a/java/src/com/android/inputmethod/latin/UserDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserDictionary.java
@@ -21,18 +21,21 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
+import android.net.Uri;
import android.provider.UserDictionary.Words;
public class UserDictionary extends ExpandableDictionary {
- private static final String[] PROJECTION = {
- Words._ID,
+ private static final String[] PROJECTION_QUERY = {
Words.WORD,
- Words.FREQUENCY
+ Words.FREQUENCY,
};
- private static final int INDEX_WORD = 1;
- private static final int INDEX_FREQUENCY = 2;
+ private static final String[] PROJECTION_ADD = {
+ Words._ID,
+ Words.FREQUENCY,
+ Words.LOCALE,
+ };
private ContentObserver mObserver;
private String mLocale;
@@ -66,7 +69,7 @@ public class UserDictionary extends ExpandableDictionary {
@Override
public void loadDictionaryAsync() {
Cursor cursor = getContext().getContentResolver()
- .query(Words.CONTENT_URI, PROJECTION, "(locale IS NULL) or (locale=?)",
+ .query(Words.CONTENT_URI, PROJECTION_QUERY, "(locale IS NULL) or (locale=?)",
new String[] { mLocale }, null);
addWords(cursor);
}
@@ -80,7 +83,7 @@ public class UserDictionary extends ExpandableDictionary {
* @TODO use a higher or float range for frequency
*/
@Override
- public synchronized void addWord(String word, int frequency) {
+ public synchronized void addWord(final String word, final int frequency) {
// Force load the dictionary here synchronously
if (getRequiresReload()) loadDictionaryAsync();
// Safeguard against adding long words. Can cause stack overflow.
@@ -97,8 +100,24 @@ public class UserDictionary extends ExpandableDictionary {
final ContentResolver contentResolver = getContext().getContentResolver();
new Thread("addWord") {
+ @Override
public void run() {
- contentResolver.insert(Words.CONTENT_URI, values);
+ Cursor cursor = contentResolver.query(Words.CONTENT_URI, PROJECTION_ADD,
+ "word=? and ((locale IS NULL) or (locale=?))",
+ new String[] { word, mLocale }, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ String locale = cursor.getString(cursor.getColumnIndex(Words.LOCALE));
+ // If locale is null, we will not override the entry.
+ if (locale != null && locale.equals(mLocale.toString())) {
+ long id = cursor.getLong(cursor.getColumnIndex(Words._ID));
+ Uri uri = Uri.withAppendedPath(Words.CONTENT_URI, Long.toString(id));
+ // Update the entry with new frequency value.
+ contentResolver.update(uri, values, null, null);
+ }
+ } else {
+ // Insert new entry.
+ contentResolver.insert(Words.CONTENT_URI, values);
+ }
}
}.start();
@@ -107,9 +126,8 @@ public class UserDictionary extends ExpandableDictionary {
}
@Override
- public synchronized void getWords(final WordComposer codes, final WordCallback callback,
- int[] nextLettersFrequencies) {
- super.getWords(codes, callback, nextLettersFrequencies);
+ public synchronized void getWords(final WordComposer codes, final WordCallback callback) {
+ super.getWords(codes, callback);
}
@Override
@@ -119,12 +137,14 @@ public class UserDictionary extends ExpandableDictionary {
private void addWords(Cursor cursor) {
clearDictionary();
-
+ if (cursor == null) return;
final int maxWordLength = getMaxWordLength();
if (cursor.moveToFirst()) {
+ final int indexWord = cursor.getColumnIndex(Words.WORD);
+ final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
while (!cursor.isAfterLast()) {
- String word = cursor.getString(INDEX_WORD);
- int frequency = cursor.getInt(INDEX_FREQUENCY);
+ String word = cursor.getString(indexWord);
+ int frequency = cursor.getInt(indexFrequency);
// Safeguard against adding really long words. Stack may overflow due
// to recursion
if (word.length() < maxWordLength) {
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
new file mode 100644
index 000000000..6bdc0a857
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -0,0 +1,718 @@
+/*
+ * 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.latin;
+
+import com.android.inputmethod.compat.InputMethodInfoCompatWrapper;
+import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper;
+import com.android.inputmethod.compat.InputTypeCompatUtils;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.inputmethodservice.InputMethodService;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.text.InputType;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+public class Utils {
+ private static final String TAG = Utils.class.getSimpleName();
+ private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
+ private static boolean DBG = LatinImeLogger.sDBG;
+ private static boolean DBG_EDIT_DISTANCE = false;
+
+ private Utils() {
+ // Intentional empty constructor for utility class.
+ }
+
+ /**
+ * Cancel an {@link AsyncTask}.
+ *
+ * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
+ * task should be interrupted; otherwise, in-progress tasks are allowed
+ * to complete.
+ */
+ public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
+ if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
+ task.cancel(mayInterruptIfRunning);
+ }
+ }
+
+ public static class GCUtils {
+ private static final String GC_TAG = GCUtils.class.getSimpleName();
+ public static final int GC_TRY_COUNT = 2;
+ // GC_TRY_LOOP_MAX is used for the hard limit of GC wait,
+ // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT.
+ public static final int GC_TRY_LOOP_MAX = 5;
+ private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS;
+ private static GCUtils sInstance = new GCUtils();
+ private int mGCTryCount = 0;
+
+ public static GCUtils getInstance() {
+ return sInstance;
+ }
+
+ public void reset() {
+ mGCTryCount = 0;
+ }
+
+ public boolean tryGCOrWait(String metaData, Throwable t) {
+ if (mGCTryCount == 0) {
+ System.gc();
+ }
+ if (++mGCTryCount > GC_TRY_COUNT) {
+ LatinImeLogger.logOnException(metaData, t);
+ return false;
+ } else {
+ try {
+ Thread.sleep(GC_INTERVAL);
+ return true;
+ } catch (InterruptedException e) {
+ Log.e(GC_TAG, "Sleep was interrupted.");
+ LatinImeLogger.logOnException(metaData, t);
+ return false;
+ }
+ }
+ }
+ }
+
+ public static boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManagerCompatWrapper imm) {
+ final List<InputMethodInfoCompatWrapper> enabledImis = imm.getEnabledInputMethodList();
+
+ // Filters out IMEs that have auxiliary subtypes only (including either implicitly or
+ // explicitly enabled ones).
+ final ArrayList<InputMethodInfoCompatWrapper> filteredImis =
+ new ArrayList<InputMethodInfoCompatWrapper>();
+
+ outerloop:
+ for (InputMethodInfoCompatWrapper imi : enabledImis) {
+ // We can return true immediately after we find two or more filtered IMEs.
+ if (filteredImis.size() > 1) return true;
+ final List<InputMethodSubtypeCompatWrapper> subtypes =
+ imm.getEnabledInputMethodSubtypeList(imi, true);
+ // IMEs that have no subtypes should be included.
+ if (subtypes.isEmpty()) {
+ filteredImis.add(imi);
+ continue;
+ }
+ // IMEs that have one or more non-auxiliary subtypes should be included.
+ for (InputMethodSubtypeCompatWrapper subtype : subtypes) {
+ if (!subtype.isAuxiliary()) {
+ filteredImis.add(imi);
+ continue outerloop;
+ }
+ }
+ }
+
+ return filteredImis.size() > 1
+ // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled
+ // input method subtype (The current IME should be LatinIME.)
+ || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
+ }
+
+ public static String getInputMethodId(InputMethodManagerCompatWrapper imm, String packageName) {
+ return getInputMethodInfo(imm, packageName).getId();
+ }
+
+ public static InputMethodInfoCompatWrapper getInputMethodInfo(
+ InputMethodManagerCompatWrapper imm, String packageName) {
+ for (final InputMethodInfoCompatWrapper imi : imm.getEnabledInputMethodList()) {
+ if (imi.getPackageName().equals(packageName))
+ return imi;
+ }
+ throw new RuntimeException("Can not find input method id for " + packageName);
+ }
+
+ public static boolean shouldBlockedBySafetyNetForAutoCorrection(SuggestedWords suggestions,
+ Suggest suggest) {
+ // Safety net for auto correction.
+ // Actually if we hit this safety net, it's actually a bug.
+ if (suggestions.size() <= 1 || suggestions.mTypedWordValid) return false;
+ // If user selected aggressive auto correction mode, there is no need to use the safety
+ // net.
+ if (suggest.isAggressiveAutoCorrectionMode()) return false;
+ CharSequence typedWord = suggestions.getWord(0);
+ // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
+ // we should not use net because relatively edit distance can be big.
+ if (typedWord.length() < MINIMUM_SAFETY_NET_CHAR_LENGTH) return false;
+ CharSequence candidateWord = suggestions.getWord(1);
+ final int typedWordLength = typedWord.length();
+ final int maxEditDistanceOfNativeDictionary = typedWordLength < 5 ? 2 : typedWordLength / 2;
+ final int distance = Utils.editDistance(typedWord, candidateWord);
+ if (DBG) {
+ Log.d(TAG, "Autocorrected edit distance = " + distance
+ + ", " + maxEditDistanceOfNativeDictionary);
+ }
+ if (distance > maxEditDistanceOfNativeDictionary) {
+ if (DBG) {
+ Log.d(TAG, "Safety net: before = " + typedWord + ", after = " + candidateWord);
+ Log.w(TAG, "(Error) The edit distance of this correction exceeds limit. "
+ + "Turning off auto-correction.");
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /* package */ static class RingCharBuffer {
+ private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
+ private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
+ private static final int INVALID_COORDINATE = -2;
+ /* package */ static final int BUFSIZE = 20;
+ private InputMethodService mContext;
+ private boolean mEnabled = false;
+ private boolean mUsabilityStudy = false;
+ private int mEnd = 0;
+ /* package */ int mLength = 0;
+ private char[] mCharBuf = new char[BUFSIZE];
+ private int[] mXBuf = new int[BUFSIZE];
+ private int[] mYBuf = new int[BUFSIZE];
+
+ private RingCharBuffer() {
+ // Intentional empty constructor for singleton.
+ }
+ public static RingCharBuffer getInstance() {
+ return sRingCharBuffer;
+ }
+ public static RingCharBuffer init(InputMethodService context, boolean enabled,
+ boolean usabilityStudy) {
+ sRingCharBuffer.mContext = context;
+ sRingCharBuffer.mEnabled = enabled || usabilityStudy;
+ sRingCharBuffer.mUsabilityStudy = usabilityStudy;
+ UsabilityStudyLogUtils.getInstance().init(context);
+ return sRingCharBuffer;
+ }
+ private int normalize(int in) {
+ int ret = in % BUFSIZE;
+ return ret < 0 ? ret + BUFSIZE : ret;
+ }
+ public void push(char c, int x, int y) {
+ if (!mEnabled) return;
+ if (mUsabilityStudy) {
+ UsabilityStudyLogUtils.getInstance().writeChar(c, x, y);
+ }
+ mCharBuf[mEnd] = c;
+ mXBuf[mEnd] = x;
+ mYBuf[mEnd] = y;
+ mEnd = normalize(mEnd + 1);
+ if (mLength < BUFSIZE) {
+ ++mLength;
+ }
+ }
+ public char pop() {
+ if (mLength < 1) {
+ return PLACEHOLDER_DELIMITER_CHAR;
+ } else {
+ mEnd = normalize(mEnd - 1);
+ --mLength;
+ return mCharBuf[mEnd];
+ }
+ }
+ public char getBackwardNthChar(int n) {
+ if (mLength <= n || n < 0) {
+ return PLACEHOLDER_DELIMITER_CHAR;
+ } else {
+ return mCharBuf[normalize(mEnd - n - 1)];
+ }
+ }
+ public int getPreviousX(char c, int back) {
+ int index = normalize(mEnd - 2 - back);
+ if (mLength <= back
+ || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
+ return INVALID_COORDINATE;
+ } else {
+ return mXBuf[index];
+ }
+ }
+ public int getPreviousY(char c, int back) {
+ int index = normalize(mEnd - 2 - back);
+ if (mLength <= back
+ || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
+ return INVALID_COORDINATE;
+ } else {
+ return mYBuf[index];
+ }
+ }
+ public String getLastWord(int ignoreCharCount) {
+ StringBuilder sb = new StringBuilder();
+ int i = ignoreCharCount;
+ for (; i < mLength; ++i) {
+ char c = mCharBuf[normalize(mEnd - 1 - i)];
+ if (!((LatinIME)mContext).isWordSeparator(c)) {
+ break;
+ }
+ }
+ for (; i < mLength; ++i) {
+ char c = mCharBuf[normalize(mEnd - 1 - i)];
+ if (!((LatinIME)mContext).isWordSeparator(c)) {
+ sb.append(c);
+ } else {
+ break;
+ }
+ }
+ return sb.reverse().toString();
+ }
+ public void reset() {
+ mLength = 0;
+ }
+ }
+
+
+ /* Damerau-Levenshtein distance */
+ public static int editDistance(CharSequence s, CharSequence t) {
+ if (s == null || t == null) {
+ throw new IllegalArgumentException("editDistance: Arguments should not be null.");
+ }
+ final int sl = s.length();
+ final int tl = t.length();
+ int[][] dp = new int [sl + 1][tl + 1];
+ for (int i = 0; i <= sl; i++) {
+ dp[i][0] = i;
+ }
+ for (int j = 0; j <= tl; j++) {
+ dp[0][j] = j;
+ }
+ for (int i = 0; i < sl; ++i) {
+ for (int j = 0; j < tl; ++j) {
+ final char sc = Character.toLowerCase(s.charAt(i));
+ final char tc = Character.toLowerCase(t.charAt(j));
+ final int cost = sc == tc ? 0 : 1;
+ dp[i + 1][j + 1] = Math.min(
+ dp[i][j + 1] + 1, Math.min(dp[i + 1][j] + 1, dp[i][j] + cost));
+ // Overwrite for transposition cases
+ if (i > 0 && j > 0
+ && sc == Character.toLowerCase(t.charAt(j - 1))
+ && tc == Character.toLowerCase(s.charAt(i - 1))) {
+ dp[i + 1][j + 1] = Math.min(dp[i + 1][j + 1], dp[i - 1][j - 1] + cost);
+ }
+ }
+ }
+ if (DBG_EDIT_DISTANCE) {
+ Log.d(TAG, "editDistance:" + s + "," + t);
+ for (int i = 0; i < dp.length; ++i) {
+ StringBuffer sb = new StringBuffer();
+ for (int j = 0; j < dp[i].length; ++j) {
+ sb.append(dp[i][j]).append(',');
+ }
+ Log.d(TAG, i + ":" + sb.toString());
+ }
+ }
+ return dp[sl][tl];
+ }
+
+ // Get the current stack trace
+ public static String getStackTrace() {
+ StringBuilder sb = new StringBuilder();
+ try {
+ throw new RuntimeException();
+ } catch (RuntimeException e) {
+ StackTraceElement[] frames = e.getStackTrace();
+ // Start at 1 because the first frame is here and we don't care about it
+ for (int j = 1; j < frames.length; ++j) sb.append(frames[j].toString() + "\n");
+ }
+ return sb.toString();
+ }
+
+ // In dictionary.cpp, getSuggestion() method,
+ // suggestion scores are computed using the below formula.
+ // original score
+ // := pow(mTypedLetterMultiplier (this is defined 2),
+ // (the number of matched characters between typed word and suggested word))
+ // * (individual word's score which defined in the unigram dictionary,
+ // and this score is defined in range [0, 255].)
+ // Then, the following processing is applied.
+ // - If the dictionary word is matched up to the point of the user entry
+ // (full match up to min(before.length(), after.length())
+ // => Then multiply by FULL_MATCHED_WORDS_PROMOTION_RATE (this is defined 1.2)
+ // - If the word is a true full match except for differences in accents or
+ // capitalization, then treat it as if the score was 255.
+ // - If before.length() == after.length()
+ // => multiply by mFullWordMultiplier (this is defined 2))
+ // So, maximum original score is pow(2, min(before.length(), after.length())) * 255 * 2 * 1.2
+ // For historical reasons we ignore the 1.2 modifier (because the measure for a good
+ // autocorrection threshold was done at a time when it didn't exist). This doesn't change
+ // the result.
+ // So, we can normalize original score by dividing pow(2, min(b.l(),a.l())) * 255 * 2.
+ private static final int MAX_INITIAL_SCORE = 255;
+ private static final int TYPED_LETTER_MULTIPLIER = 2;
+ private static final int FULL_WORD_MULTIPLIER = 2;
+ private static final int S_INT_MAX = 2147483647;
+ public static double calcNormalizedScore(CharSequence before, CharSequence after, int score) {
+ final int beforeLength = before.length();
+ final int afterLength = after.length();
+ if (beforeLength == 0 || afterLength == 0) return 0;
+ final int distance = editDistance(before, after);
+ // If afterLength < beforeLength, the algorithm is suggesting a word by excessive character
+ // correction.
+ int spaceCount = 0;
+ for (int i = 0; i < afterLength; ++i) {
+ if (after.charAt(i) == Keyboard.CODE_SPACE) {
+ ++spaceCount;
+ }
+ }
+ if (spaceCount == afterLength) return 0;
+ final double maximumScore = score == S_INT_MAX ? S_INT_MAX : MAX_INITIAL_SCORE
+ * Math.pow(
+ TYPED_LETTER_MULTIPLIER, Math.min(beforeLength, afterLength - spaceCount))
+ * FULL_WORD_MULTIPLIER;
+ // add a weight based on edit distance.
+ // distance <= max(afterLength, beforeLength) == afterLength,
+ // so, 0 <= distance / afterLength <= 1
+ final double weight = 1.0 - (double) distance / afterLength;
+ return (score / maximumScore) * weight;
+ }
+
+ public static class UsabilityStudyLogUtils {
+ private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
+ private static final String FILENAME = "log.txt";
+ private static final UsabilityStudyLogUtils sInstance =
+ new UsabilityStudyLogUtils();
+ private final Handler mLoggingHandler;
+ private File mFile;
+ private File mDirectory;
+ private InputMethodService mIms;
+ private PrintWriter mWriter;
+ private final Date mDate;
+ private final SimpleDateFormat mDateFormat;
+
+ private UsabilityStudyLogUtils() {
+ mDate = new Date();
+ mDateFormat = new SimpleDateFormat("dd MMM HH:mm:ss.SSS");
+
+ HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ handlerThread.start();
+ mLoggingHandler = new Handler(handlerThread.getLooper());
+ }
+
+ public static UsabilityStudyLogUtils getInstance() {
+ return sInstance;
+ }
+
+ public void init(InputMethodService ims) {
+ mIms = ims;
+ mDirectory = ims.getFilesDir();
+ }
+
+ private void createLogFileIfNotExist() {
+ if ((mFile == null || !mFile.exists())
+ && (mDirectory != null && mDirectory.exists())) {
+ try {
+ mWriter = getPrintWriter(mDirectory, FILENAME, false);
+ } catch (IOException e) {
+ Log.e(USABILITY_TAG, "Can't create log file.");
+ }
+ }
+ }
+
+ public void writeBackSpace() {
+ UsabilityStudyLogUtils.getInstance().write("<backspace>\t0\t0");
+ }
+
+ public void writeChar(char c, int x, int y) {
+ String inputChar = String.valueOf(c);
+ switch (c) {
+ case '\n':
+ inputChar = "<enter>";
+ break;
+ case '\t':
+ inputChar = "<tab>";
+ break;
+ case ' ':
+ inputChar = "<space>";
+ break;
+ }
+ UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
+ LatinImeLogger.onPrintAllUsabilityStudyLogs();
+ }
+
+ public void write(final String log) {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ createLogFileIfNotExist();
+ final long currentTime = System.currentTimeMillis();
+ mDate.setTime(currentTime);
+
+ final String printString = String.format("%s\t%d\t%s\n",
+ mDateFormat.format(mDate), currentTime, log);
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Write: " + log);
+ }
+ mWriter.print(printString);
+ }
+ });
+ }
+
+ public void printAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mWriter.flush();
+ StringBuilder sb = new StringBuilder();
+ BufferedReader br = getBufferedReader();
+ String line;
+ try {
+ while ((line = br.readLine()) != null) {
+ sb.append('\n');
+ sb.append(line);
+ }
+ } catch (IOException e) {
+ Log.e(USABILITY_TAG, "Can't read log file.");
+ } finally {
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "output all logs\n" + sb.toString());
+ }
+ mIms.getCurrentInputConnection().commitText(sb.toString(), 0);
+ try {
+ br.close();
+ } catch (IOException e) {
+ // ignore.
+ }
+ }
+ }
+ });
+ }
+
+ public void clearAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mFile != null && mFile.exists()) {
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Delete log file.");
+ }
+ mFile.delete();
+ mWriter.close();
+ }
+ }
+ });
+ }
+
+ private BufferedReader getBufferedReader() {
+ createLogFileIfNotExist();
+ try {
+ return new BufferedReader(new FileReader(mFile));
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ private PrintWriter getPrintWriter(
+ File dir, String filename, boolean renew) throws IOException {
+ mFile = new File(dir, filename);
+ if (mFile.exists()) {
+ if (renew) {
+ mFile.delete();
+ }
+ }
+ return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */);
+ }
+ }
+
+ public static int getKeyboardMode(EditorInfo attribute) {
+ if (attribute == null)
+ return KeyboardId.MODE_TEXT;
+
+ final int inputType = attribute.inputType;
+ final int variation = inputType & InputType.TYPE_MASK_VARIATION;
+
+ switch (inputType & InputType.TYPE_MASK_CLASS) {
+ case InputType.TYPE_CLASS_NUMBER:
+ case InputType.TYPE_CLASS_DATETIME:
+ return KeyboardId.MODE_NUMBER;
+ case InputType.TYPE_CLASS_PHONE:
+ return KeyboardId.MODE_PHONE;
+ case InputType.TYPE_CLASS_TEXT:
+ if (InputTypeCompatUtils.isEmailVariation(variation)) {
+ return KeyboardId.MODE_EMAIL;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
+ return KeyboardId.MODE_URL;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
+ return KeyboardId.MODE_IM;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+ return KeyboardId.MODE_TEXT;
+ } else {
+ return KeyboardId.MODE_TEXT;
+ }
+ default:
+ return KeyboardId.MODE_TEXT;
+ }
+ }
+
+ public static boolean containsInCsv(String key, String csv) {
+ if (csv == null)
+ return false;
+ for (String option : csv.split(",")) {
+ if (option.equals(key))
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean inPrivateImeOptions(String packageName, String key,
+ EditorInfo attribute) {
+ if (attribute == null)
+ return false;
+ return containsInCsv(packageName != null ? packageName + "." + key : key,
+ attribute.privateImeOptions);
+ }
+
+ /**
+ * Returns a main dictionary resource id
+ * @return main dictionary resource id
+ */
+ public static int getMainDictionaryResourceId(Resources res) {
+ final String MAIN_DIC_NAME = "main";
+ String packageName = LatinIME.class.getPackage().getName();
+ return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName);
+ }
+
+ public static void loadNativeLibrary() {
+ try {
+ System.loadLibrary("jni_latinime");
+ } catch (UnsatisfiedLinkError ule) {
+ Log.e(TAG, "Could not load native library jni_latinime");
+ }
+ }
+
+ /**
+ * Returns true if a and b are equal ignoring the case of the character.
+ * @param a first character to check
+ * @param b second character to check
+ * @return {@code true} if a and b are equal, {@code false} otherwise.
+ */
+ public static boolean equalsIgnoreCase(char a, char b) {
+ // Some language, such as Turkish, need testing both cases.
+ return a == b
+ || Character.toLowerCase(a) == Character.toLowerCase(b)
+ || Character.toUpperCase(a) == Character.toUpperCase(b);
+ }
+
+ /**
+ * Returns true if a and b are equal ignoring the case of the characters, including if they are
+ * both null.
+ * @param a first CharSequence to check
+ * @param b second CharSequence to check
+ * @return {@code true} if a and b are equal, {@code false} otherwise.
+ */
+ public static boolean equalsIgnoreCase(CharSequence a, CharSequence b) {
+ if (a == b)
+ return true; // including both a and b are null.
+ if (a == null || b == null)
+ return false;
+ final int length = a.length();
+ if (length != b.length())
+ return false;
+ for (int i = 0; i < length; i++) {
+ if (!equalsIgnoreCase(a.charAt(i), b.charAt(i)))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if a and b are equal ignoring the case of the characters, including if a is null
+ * and b is zero length.
+ * @param a CharSequence to check
+ * @param b character array to check
+ * @param offset start offset of array b
+ * @param length length of characters in array b
+ * @return {@code true} if a and b are equal, {@code false} otherwise.
+ * @throws IndexOutOfBoundsException
+ * if {@code offset < 0 || length < 0 || offset + length > data.length}.
+ * @throws NullPointerException if {@code b == null}.
+ */
+ public static boolean equalsIgnoreCase(CharSequence a, char[] b, int offset, int length) {
+ if (offset < 0 || length < 0 || length > b.length - offset)
+ throw new IndexOutOfBoundsException("array.length=" + b.length + " offset=" + offset
+ + " length=" + length);
+ if (a == null)
+ return length == 0; // including a is null and b is zero length.
+ if (a.length() != length)
+ return false;
+ for (int i = 0; i < length; i++) {
+ if (!equalsIgnoreCase(a.charAt(i), b[offset + i]))
+ return false;
+ }
+ return true;
+ }
+
+ public static float getDipScale(Context context) {
+ final float scale = context.getResources().getDisplayMetrics().density;
+ return scale;
+ }
+
+ /** Convert pixel to DIP */
+ public static int dipToPixel(float scale, int dip) {
+ return (int) (dip * scale + 0.5);
+ }
+
+ public static Locale setSystemLocale(Resources res, Locale newLocale) {
+ final Configuration conf = res.getConfiguration();
+ final Locale saveLocale = conf.locale;
+ conf.locale = newLocale;
+ res.updateConfiguration(conf, res.getDisplayMetrics());
+ return saveLocale;
+ }
+
+ private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>();
+
+ public static Locale constructLocaleFromString(String localeStr) {
+ if (localeStr == null)
+ return null;
+ synchronized (sLocaleCache) {
+ if (sLocaleCache.containsKey(localeStr))
+ return sLocaleCache.get(localeStr);
+ Locale retval = null;
+ String[] localeParams = localeStr.split("_", 3);
+ if (localeParams.length == 1) {
+ retval = new Locale(localeParams[0]);
+ } else if (localeParams.length == 2) {
+ retval = new Locale(localeParams[0], localeParams[1]);
+ } else if (localeParams.length == 3) {
+ retval = new Locale(localeParams[0], localeParams[1], localeParams[2]);
+ }
+ if (retval != null) {
+ sLocaleCache.put(localeStr, retval);
+ }
+ return retval;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
new file mode 100644
index 000000000..4377373d2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
@@ -0,0 +1,100 @@
+/*
+ * 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.latin;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.HashMap;
+
+public class WhitelistDictionary extends Dictionary {
+
+ private static final boolean DBG = LatinImeLogger.sDBG;
+ private static final String TAG = WhitelistDictionary.class.getSimpleName();
+
+ private final HashMap<String, Pair<Integer, String>> mWhitelistWords =
+ new HashMap<String, Pair<Integer, String>>();
+
+ private static final WhitelistDictionary sInstance = new WhitelistDictionary();
+
+ private WhitelistDictionary() {
+ }
+
+ public static WhitelistDictionary init(Context context) {
+ synchronized (sInstance) {
+ if (context != null) {
+ // Wordlist is initialized by the proper language in Suggestion.java#init
+ sInstance.initWordlist(
+ context.getResources().getStringArray(R.array.wordlist_whitelist));
+ } else {
+ sInstance.mWhitelistWords.clear();
+ }
+ }
+ return sInstance;
+ }
+
+ private void initWordlist(String[] wordlist) {
+ mWhitelistWords.clear();
+ final int N = wordlist.length;
+ if (N % 3 != 0) {
+ if (DBG) {
+ Log.d(TAG, "The number of the whitelist is invalid.");
+ }
+ return;
+ }
+ try {
+ for (int i = 0; i < N; i += 3) {
+ final int score = Integer.valueOf(wordlist[i]);
+ final String before = wordlist[i + 1];
+ final String after = wordlist[i + 2];
+ if (before != null && after != null) {
+ mWhitelistWords.put(
+ before.toLowerCase(), new Pair<Integer, String>(score, after));
+ }
+ }
+ } catch (NumberFormatException e) {
+ if (DBG) {
+ Log.d(TAG, "The score of the word is invalid.");
+ }
+ }
+ }
+
+ public String getWhiteListedWord(String before) {
+ if (before == null) return null;
+ final String lowerCaseBefore = before.toLowerCase();
+ if(mWhitelistWords.containsKey(lowerCaseBefore)) {
+ if (DBG) {
+ Log.d(TAG, "--- found whiteListedWord: " + lowerCaseBefore);
+ }
+ return mWhitelistWords.get(lowerCaseBefore).second;
+ }
+ return null;
+ }
+
+ // Not used for WhitelistDictionary. We use getWhitelistedWord() in Suggest.java instead
+ @Override
+ public void getWords(WordComposer composer, WordCallback callback) {
+ }
+
+ @Override
+ public boolean isValidWord(CharSequence word) {
+ if (TextUtils.isEmpty(word)) return false;
+ return !TextUtils.isEmpty(getWhiteListedWord(word.toString()));
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 2e415b771..af5e4b179 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -16,23 +16,33 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.keyboard.KeyDetector;
+
import java.util.ArrayList;
/**
* A place to store the currently composing word with information such as adjacent key codes as well
*/
public class WordComposer {
+
+ public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
+ public static final int NOT_A_COORDINATE = -1;
+
/**
* The list of unicode values for each keystroke (including surrounding keys)
*/
- private final ArrayList<int[]> mCodes;
-
+ private ArrayList<int[]> mCodes;
+
+ private int mTypedLength;
+ private int[] mXCoordinates;
+ private int[] mYCoordinates;
+
/**
* The word chosen from the candidate list, until it is committed.
*/
private String mPreferredWord;
-
- private final StringBuilder mTypedWord;
+
+ private StringBuilder mTypedWord;
private int mCapsCount;
@@ -44,17 +54,28 @@ public class WordComposer {
private boolean mIsFirstCharCapitalized;
public WordComposer() {
- mCodes = new ArrayList<int[]>(12);
- mTypedWord = new StringBuilder(20);
+ final int N = BinaryDictionary.MAX_WORD_LENGTH;
+ mCodes = new ArrayList<int[]>(N);
+ mTypedWord = new StringBuilder(N);
+ mTypedLength = 0;
+ mXCoordinates = new int[N];
+ mYCoordinates = new int[N];
}
- WordComposer(WordComposer copy) {
- mCodes = new ArrayList<int[]>(copy.mCodes);
- mPreferredWord = copy.mPreferredWord;
- mTypedWord = new StringBuilder(copy.mTypedWord);
- mCapsCount = copy.mCapsCount;
- mAutoCapitalized = copy.mAutoCapitalized;
- mIsFirstCharCapitalized = copy.mIsFirstCharCapitalized;
+ public WordComposer(WordComposer source) {
+ init(source);
+ }
+
+ public void init(WordComposer source) {
+ mCodes = new ArrayList<int[]>(source.mCodes);
+ mPreferredWord = source.mPreferredWord;
+ mTypedWord = new StringBuilder(source.mTypedWord);
+ mCapsCount = source.mCapsCount;
+ mAutoCapitalized = source.mAutoCapitalized;
+ mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
+ mTypedLength = source.mTypedLength;
+ mXCoordinates = source.mXCoordinates;
+ mYCoordinates = source.mYCoordinates;
}
/**
@@ -62,6 +83,7 @@ public class WordComposer {
*/
public void reset() {
mCodes.clear();
+ mTypedLength = 0;
mIsFirstCharCapitalized = false;
mPreferredWord = null;
mTypedWord.setLength(0);
@@ -85,15 +107,28 @@ public class WordComposer {
return mCodes.get(index);
}
+ public int[] getXCoordinates() {
+ return mXCoordinates;
+ }
+
+ public int[] getYCoordinates() {
+ return mYCoordinates;
+ }
+
/**
* Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of
* the array containing unicode for adjacent keys, sorted by reducing probability/proximity.
* @param codes the array of unicode values
*/
- public void add(int primaryCode, int[] codes) {
+ public void add(int primaryCode, int[] codes, int x, int y) {
mTypedWord.append((char) primaryCode);
correctPrimaryJuxtapos(primaryCode, codes);
mCodes.add(codes);
+ if (mTypedLength < BinaryDictionary.MAX_WORD_LENGTH) {
+ mXCoordinates[mTypedLength] = x;
+ mYCoordinates[mTypedLength] = y;
+ }
+ ++mTypedLength;
if (Character.isUpperCase((char) primaryCode)) mCapsCount++;
}
@@ -124,6 +159,9 @@ public class WordComposer {
mTypedWord.deleteCharAt(lastPos);
if (Character.isUpperCase(last)) mCapsCount--;
}
+ if (mTypedLength > 0) {
+ --mTypedLength;
+ }
}
/**
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java
new file mode 100644
index 000000000..63c6d69d7
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java
@@ -0,0 +1,115 @@
+/*
+ * 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.latin.spellcheck;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.inputmethod.compat.ArraysCompatUtils;
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.Dictionary.DataType;
+import com.android.inputmethod.latin.Dictionary.WordCallback;
+import com.android.inputmethod.latin.DictionaryFactory;
+import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.WordComposer;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Implements spell checking methods.
+ */
+public class SpellChecker {
+
+ public final Dictionary mDictionary;
+
+ public SpellChecker(final Context context, final Locale locale) {
+ final Resources resources = context.getResources();
+ final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources);
+ mDictionary = DictionaryFactory.createDictionaryFromManager(context, locale,
+ fallbackResourceId);
+ }
+
+ // Note : this must be reentrant
+ /**
+ * Finds out whether a word is in the dictionary or not.
+ *
+ * @param text the sequence containing the word to check for.
+ * @param start the index of the first character of the word in text.
+ * @param end the index of the next-to-last character in text.
+ * @return true if the word is in the dictionary, false otherwise.
+ */
+ public boolean isCorrect(final CharSequence text, final int start, final int end) {
+ return mDictionary.isValidWord(text.subSequence(start, end));
+ }
+
+ private static class SuggestionsGatherer implements WordCallback {
+ private final int DEFAULT_SUGGESTION_LENGTH = 16;
+ private final List<String> mSuggestions = new LinkedList<String>();
+ private int[] mScores = new int[DEFAULT_SUGGESTION_LENGTH];
+ private int mLength = 0;
+
+ @Override
+ synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
+ int dicTypeId, DataType dataType) {
+ if (mLength >= mScores.length) {
+ final int newLength = mScores.length * 2;
+ mScores = new int[newLength];
+ }
+ final int positionIndex = ArraysCompatUtils.binarySearch(mScores, 0, mLength, score);
+ // binarySearch returns the index if the element exists, and -<insertion index> - 1
+ // if it doesn't. See documentation for binarySearch.
+ final int insertionIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
+ System.arraycopy(mScores, insertionIndex, mScores, insertionIndex + 1,
+ mLength - insertionIndex);
+ mLength += 1;
+ mScores[insertionIndex] = score;
+ mSuggestions.add(insertionIndex, new String(word, wordOffset, wordLength));
+ return true;
+ }
+
+ public List<String> getGatheredSuggestions() {
+ return mSuggestions;
+ }
+ }
+
+ // Note : this must be reentrant
+ /**
+ * Gets a list of suggestions for a specific string.
+ *
+ * This returns a list of possible corrections for the text passed as an
+ * arguments. It may split or group words, and even perform grammatical
+ * analysis.
+ *
+ * @param text the sequence containing the word to check for.
+ * @param start the index of the first character of the word in text.
+ * @param end the index of the next-to-last character in text.
+ * @return a list of possible suggestions to replace the text.
+ */
+ public List<String> getSuggestions(final CharSequence text, final int start, final int end) {
+ final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer();
+ final WordComposer composer = new WordComposer();
+ for (int i = start; i < end; ++i) {
+ int character = text.charAt(i);
+ composer.add(character, new int[] { character },
+ WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
+ }
+ mDictionary.getWords(composer, suggestionsGatherer);
+ return suggestionsGatherer.getGatheredSuggestions();
+ }
+}