diff options
author | 2012-08-06 16:04:07 +0900 | |
---|---|---|
committer | 2012-08-06 16:04:07 +0900 | |
commit | 44456dcbf23c29b54e9372d932b79805957724cb (patch) | |
tree | a8167c70e9d7017f212066cf2364d1ad5b09fdca /java/src | |
parent | 07e4f9eb530a795efd97986e28546c8c5e895c72 (diff) | |
parent | d299bde24b4afd3955fa65c7ab372807a336c6df (diff) | |
download | latinime-44456dcbf23c29b54e9372d932b79805957724cb.tar.gz latinime-44456dcbf23c29b54e9372d932b79805957724cb.tar.xz latinime-44456dcbf23c29b54e9372d932b79805957724cb.zip |
Merge remote-tracking branch 'goog/master' into mergescriptpackage
Conflicts:
java/res/values-af/strings.xml
java/res/values-cs/strings.xml
java/res/values-et/strings.xml
java/res/values-fi/strings.xml
java/res/values-hi/strings.xml
java/res/values-hr/strings.xml
java/res/values-hu/strings.xml
java/res/values-in/strings.xml
java/res/values-ja/strings.xml
java/res/values-ko/strings.xml
java/res/values-lt/strings.xml
java/res/values-lv/strings.xml
java/res/values-ms/strings.xml
java/res/values-pt/strings.xml
java/res/values-ro/strings.xml
java/res/values-ru/strings.xml
java/res/values-sk/strings.xml
java/res/values-sl/strings.xml
java/res/values-sr/strings.xml
java/res/values-sw/strings.xml
java/res/values-tr/strings.xml
java/res/values-vi/strings.xml
java/res/values-zh-rCN/strings.xml
java/res/values-zh-rTW/strings.xml
Change-Id: Ib4141aca0b35148d62d22d4f32309f890c84303a
Diffstat (limited to 'java/src')
35 files changed, 2279 insertions, 1326 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java index 5ffd94a43..9b74070af 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -60,10 +60,8 @@ public class KeyCodeDescriptionMapper { // 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(' ', R.string.spoken_description_space); - // Special non-character codes defined in Keyboard + mKeyCodeMap.put(Keyboard.CODE_SPACE, R.string.spoken_description_space); 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); @@ -71,6 +69,9 @@ public class KeyCodeDescriptionMapper { 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); + mKeyCodeMap.put(Keyboard.CODE_LANGUAGE_SWITCH, R.string.spoken_description_language_switch); + mKeyCodeMap.put(Keyboard.CODE_ACTION_NEXT, R.string.spoken_description_action_next); + mKeyCodeMap.put(Keyboard.CODE_ACTION_PREVIOUS, R.string.spoken_description_action_previous); } /** @@ -274,8 +275,7 @@ public class KeyCodeDescriptionMapper { return context.getString(OBSCURED_KEY_RES_ID); } - final int resId = mKeyCodeMap.get(code); - if (resId != 0) { + if (mKeyCodeMap.indexOfKey(code) >= 0) { return context.getString(mKeyCodeMap.get(code)); } else if (isDefinedNonCtrl) { return Character.toString((char) code); diff --git a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java new file mode 100644 index 000000000..0befa7a66 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012 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 java.lang.reflect.Method; + +public class InputMethodServiceCompatUtils { + private static final Method METHOD_enableHardwareAcceleration = + CompatUtils.getMethod(InputMethodService.class, "enableHardwareAcceleration"); + + private InputMethodServiceCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static boolean enableHardwareAcceleration(InputMethodService ims) { + return (Boolean)CompatUtils.invoke(ims, false, METHOD_enableHardwareAcceleration); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index e1e1ca9cf..178c9ff05 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -414,8 +414,14 @@ public class Key { @Override public String toString() { - return String.format("%s/%s %d,%d %dx%d %s/%s/%s", - Keyboard.printableCode(mCode), mLabel, mX, mY, mWidth, mHeight, mHintLabel, + final String label; + if (StringUtils.codePointCount(mLabel) == 1 && mLabel.codePointAt(0) == mCode) { + label = ""; + } else { + label = "/" + mLabel; + } + return String.format("%s%s %d,%d %dx%d %s/%s/%s", + Keyboard.printableCode(mCode), label, mX, mY, mWidth, mHeight, mHintLabel, KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType)); } diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java index 9abc79d3c..97d88af4a 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java +++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java @@ -25,7 +25,6 @@ public class KeyDetector { private Keyboard mKeyboard; private int mCorrectionX; private int mCorrectionY; - private boolean mProximityCorrectOn; /** * This class handles key detection. @@ -38,8 +37,9 @@ public class KeyDetector { } public void setKeyboard(Keyboard keyboard, float correctionX, float correctionY) { - if (keyboard == null) + if (keyboard == null) { throw new NullPointerException(); + } mCorrectionX = (int)correctionX; mCorrectionY = (int)correctionY; mKeyboard = keyboard; @@ -59,19 +59,9 @@ public class KeyDetector { } public Keyboard getKeyboard() { - if (mKeyboard == null) - throw new IllegalStateException("keyboard isn't set"); return mKeyboard; } - public void setProximityCorrectionEnabled(boolean enabled) { - mProximityCorrectOn = enabled; - } - - public boolean isProximityCorrectionEnabled() { - return mProximityCorrectOn; - } - public boolean alwaysAllowsSlidingInput() { return false; } diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index f1a35b212..3abe890cb 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -604,9 +604,6 @@ public class Keyboard { } public float getKeyX(TypedArray keyAttr) { - final int widthType = Builder.getEnumValue(keyAttr, - R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM); - final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding; if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) { diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java index b54c72687..1e5277345 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java @@ -55,10 +55,15 @@ public class KeyboardId { public static final int ELEMENT_PHONE_SYMBOLS = 8; public static final int ELEMENT_NUMBER = 9; + public static final int FORM_FACTOR_PHONE = 0; + public static final int FORM_FACTOR_TABLET7 = 1; + public static final int FORM_FACTOR_TABLET10 = 2; + private static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1; public final InputMethodSubtype mSubtype; public final Locale mLocale; + public final int mDeviceFormFactor; public final int mOrientation; public final int mWidth; public final int mMode; @@ -72,11 +77,12 @@ public class KeyboardId { private final int mHashCode; - public KeyboardId(int elementId, InputMethodSubtype subtype, int orientation, int width, - int mode, EditorInfo editorInfo, boolean clobberSettingsKey, boolean shortcutKeyEnabled, - boolean hasShortcutKey, boolean languageSwitchKeyEnabled) { + public KeyboardId(int elementId, InputMethodSubtype subtype, int deviceFormFactor, + int orientation, int width, int mode, EditorInfo editorInfo, boolean clobberSettingsKey, + boolean shortcutKeyEnabled, boolean hasShortcutKey, boolean languageSwitchKeyEnabled) { mSubtype = subtype; mLocale = SubtypeLocale.getSubtypeLocale(subtype); + mDeviceFormFactor = deviceFormFactor; mOrientation = orientation; mWidth = width; mMode = mode; @@ -94,6 +100,7 @@ public class KeyboardId { private static int computeHashCode(KeyboardId id) { return Arrays.hashCode(new Object[] { + id.mDeviceFormFactor, id.mOrientation, id.mElementId, id.mMode, @@ -115,7 +122,8 @@ public class KeyboardId { private boolean equals(KeyboardId other) { if (other == this) return true; - return other.mOrientation == mOrientation + return other.mDeviceFormFactor == mDeviceFormFactor + && other.mOrientation == mOrientation && other.mElementId == mElementId && other.mMode == mMode && other.mWidth == mWidth @@ -184,11 +192,11 @@ public class KeyboardId { @Override public String toString() { - return String.format("[%s %s:%s %s%d %s %s %s%s%s%s%s%s%s%s]", + return String.format("[%s %s:%s %s-%s:%d %s %s %s%s%s%s%s%s%s%s]", elementIdToName(mElementId), mLocale, mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), - (mOrientation == 1 ? "port" : "land"), mWidth, + deviceFormFactor(mDeviceFormFactor), (mOrientation == 1 ? "port" : "land"), mWidth, modeName(mMode), imeAction(), (navigateNext() ? "navigateNext" : ""), @@ -226,6 +234,15 @@ public class KeyboardId { } } + public static String deviceFormFactor(int devoceFormFactor) { + switch (devoceFormFactor) { + case FORM_FACTOR_PHONE: return "phone"; + case FORM_FACTOR_TABLET7: return "tablet7"; + case FORM_FACTOR_TABLET10: return "tablet10"; + default: return null; + } + } + public static String modeName(int mode) { switch (mode) { case MODE_TEXT: return "text"; diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index aab89a3e5..64b3f0952 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -115,6 +115,7 @@ public class KeyboardLayoutSet { boolean mNoSettingsKey; boolean mLanguageSwitchKeyEnabled; InputMethodSubtype mSubtype; + int mDeviceFormFactor; int mOrientation; int mWidth; // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. @@ -211,9 +212,10 @@ public class KeyboardLayoutSet { final boolean noLanguage = SubtypeLocale.isNoLanguage(params.mSubtype); final boolean voiceKeyEnabled = params.mVoiceKeyEnabled && !noLanguage; final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != params.mVoiceKeyOnMain); - return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mOrientation, - params.mWidth, params.mMode, params.mEditorInfo, params.mNoSettingsKey, - voiceKeyEnabled, hasShortcutKey, params.mLanguageSwitchKeyEnabled); + return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mDeviceFormFactor, + params.mOrientation, params.mWidth, params.mMode, params.mEditorInfo, + params.mNoSettingsKey, voiceKeyEnabled, hasShortcutKey, + params.mLanguageSwitchKeyEnabled); } public static class Builder { @@ -239,9 +241,11 @@ public class KeyboardLayoutSet { mPackageName, NO_SETTINGS_KEY, mEditorInfo); } - public Builder setScreenGeometry(int orientation, int widthPixels) { - mParams.mOrientation = orientation; - mParams.mWidth = widthPixels; + public Builder setScreenGeometry(int deviceFormFactor, int orientation, int widthPixels) { + final Params params = mParams; + params.mDeviceFormFactor = deviceFormFactor; + params.mOrientation = orientation; + params.mWidth = widthPixels; return this; } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index d637ab5f1..dc84763c1 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -74,7 +74,6 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions { private MainKeyboardView mKeyboardView; private LatinIME mLatinIME; private Resources mResources; - private SettingsValues mCurrentSettingsValues; private KeyboardState mState; @@ -136,11 +135,11 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions { } public void loadKeyboard(EditorInfo editorInfo, SettingsValues settingsValues) { - mCurrentSettingsValues = settingsValues; final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( mThemeContext, editorInfo); - builder.setScreenGeometry(mThemeContext.getResources().getConfiguration().orientation, - mThemeContext.getResources().getDisplayMetrics().widthPixels); + final Resources res = mThemeContext.getResources(); + builder.setScreenGeometry(res.getInteger(R.integer.config_device_form_factor), + res.getConfiguration().orientation, res.getDisplayMetrics().widthPixels); builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype()); builder.setOptions( settingsValues.isVoiceKeyEnabled(editorInfo), @@ -171,20 +170,20 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions { } private void setKeyboard(final Keyboard keyboard) { - final Keyboard oldKeyboard = mKeyboardView.getKeyboard(); - mKeyboardView.setGestureInputEnabled(mCurrentSettingsValues.mGestureInputEnabled); - mKeyboardView.setKeyboard(keyboard); + final MainKeyboardView keyboardView = mKeyboardView; + final Keyboard oldKeyboard = keyboardView.getKeyboard(); + keyboardView.setKeyboard(keyboard); mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding); - mKeyboardView.setKeyPreviewPopupEnabled( + keyboardView.setKeyPreviewPopupEnabled( SettingsValues.isKeyPreviewPopupEnabled(mPrefs, mResources), SettingsValues.getKeyPreviewPopupDismissDelay(mPrefs, mResources)); - mKeyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive); - mKeyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady()); + keyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive); + keyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady()); final boolean subtypeChanged = (oldKeyboard == null) || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale); final boolean needsToDisplayLanguage = mSubtypeSwitcher.needsToDisplayLanguage( keyboard.mId.mLocale); - mKeyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage, + keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage, ImfUtils.hasMultipleEnabledIMEsOrSubtypes(mLatinIME, true)); } @@ -350,7 +349,7 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions { return mKeyboardView; } - public View onCreateInputView() { + public View onCreateInputView(boolean isHardwareAcceleratedDrawingEnabled) { if (mKeyboardView != null) { mKeyboardView.closing(); } @@ -373,6 +372,10 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions { } mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view); + if (isHardwareAcceleratedDrawingEnabled) { + mKeyboardView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + // TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off? + } mKeyboardView.setKeyboardActionListener(mLatinIME); if (mForceNonDistinctMultitouch) { mKeyboardView.setDistinctMultitouch(false); diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index f751fa53c..69e4d9805 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -25,7 +25,7 @@ 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.Region; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Message; @@ -35,9 +35,9 @@ import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.RelativeLayout; import android.widget.TextView; +import com.android.inputmethod.keyboard.internal.PreviewPlacerView; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; @@ -97,9 +97,6 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { // The maximum key label width in the proportion to the key width. private static final float MAX_LABEL_RATIO = 0.90f; - private final static int GESTURE_DRAWING_WIDTH = 5; - private final static int GESTURE_DRAWING_COLOR = 0xff33b5e5; - // Main keyboard private Keyboard mKeyboard; protected final KeyDrawParams mKeyDrawParams; @@ -109,10 +106,10 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { protected final KeyPreviewDrawParams mKeyPreviewDrawParams; private boolean mShowKeyPreviewPopup = true; private int mDelayAfterPreview; - private ViewGroup mPreviewPlacer; + private final PreviewPlacerView mPreviewPlacerView; - /** True if the gesture input is enabled. */ - protected boolean mGestureInputEnabled; + /** True if {@link KeyboardView} should handle gesture events. */ + protected boolean mShouldHandleGesture; // Drawing /** True if the entire keyboard needs to be dimmed. */ @@ -123,16 +120,15 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { private boolean mInvalidateAllKeys; /** The keys that should be drawn */ private final HashSet<Key> mInvalidatedKeys = new HashSet<Key>(); - /** The region of invalidated keys */ - private final Rect mInvalidatedKeysRect = new Rect(); - /** The region of invalidated gestures */ - private final Rect mInvalidatedGesturesRect = new Rect(); + /** The working rectangle variable */ + private final Rect mWorkingRect = new Rect(); /** The keyboard bitmap buffer for faster updates */ - private Bitmap mBuffer; + /** The clip region to draw keys */ + private final Region mClipRegion = new Region(); + private Bitmap mOffscreenBuffer; /** The canvas for the above mutable keyboard bitmap */ - private Canvas mCanvas; + private Canvas mOffscreenCanvas; private final Paint mPaint = new Paint(); - private final Paint mGesturePaint = new Paint(); private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); // This sparse array caches key label text height in pixel indexed by key label text size. private static final SparseArray<Float> sTextHeightCache = new SparseArray<Float>(); @@ -261,10 +257,12 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } public void updateKeyHeight(int keyHeight) { - if (mKeyLetterRatio >= 0.0f) + if (mKeyLetterRatio >= 0.0f) { mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); - if (mKeyLabelRatio >= 0.0f) + } + if (mKeyLabelRatio >= 0.0f) { mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio); + } mKeyLargeLabelSize = (int)(keyHeight * mKeyLargeLabelRatio); mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio); mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio); @@ -346,13 +344,16 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } public void updateKeyHeight(int keyHeight) { - mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio); - mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); + if (mPreviewTextRatio >= 0.0f) { + mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio); + } + if (mKeyLetterRatio >= 0.0f) { + mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); + } } private static void setAlpha(Drawable drawable, int alpha) { - if (drawable == null) - return; + if (drawable == null) return; drawable.setAlpha(alpha); } } @@ -377,18 +378,12 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { R.styleable.KeyboardView_verticalCorrection, 0); mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0); mBackgroundDimAlpha = a.getInt(R.styleable.KeyboardView_backgroundDimAlpha, 0); + mPreviewPlacerView = new PreviewPlacerView(context, a); a.recycle(); mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout; mPaint.setAntiAlias(true); - - // TODO: These paint parameters should be specified via attribute of the view and styleable. - mGesturePaint.setAntiAlias(true); - mGesturePaint.setStyle(Paint.Style.STROKE); - mGesturePaint.setStrokeJoin(Paint.Join.ROUND); - mGesturePaint.setColor(GESTURE_DRAWING_COLOR); - mGesturePaint.setStrokeWidth(GESTURE_DRAWING_WIDTH); } // Read fraction value in TypedArray as float. @@ -443,8 +438,11 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { return mShowKeyPreviewPopup; } - public void setGestureInputEnabled(boolean gestureInputEnabled) { - mGestureInputEnabled = gestureInputEnabled; + public void setGestureHandlingMode(boolean shouldHandleGesture, + boolean drawsGesturePreviewTrail, boolean drawsGestureFloatingPreviewText) { + mShouldHandleGesture = shouldHandleGesture; + mPreviewPlacerView.setGesturePreviewMode( + drawsGesturePreviewTrail, drawsGestureFloatingPreviewText); } @Override @@ -461,65 +459,107 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); - if (mBufferNeedsUpdate || mBuffer == null) { + if (canvas.isHardwareAccelerated()) { + onDrawKeyboard(canvas); + return; + } + if (mBufferNeedsUpdate || mOffscreenBuffer == null) { mBufferNeedsUpdate = false; - onBufferDraw(); + if (maybeAllocateOffscreenBuffer()) { + mInvalidateAllKeys = true; + // TODO: Stop using the offscreen canvas even when in software rendering + if (mOffscreenCanvas != null) { + mOffscreenCanvas.setBitmap(mOffscreenBuffer); + } else { + mOffscreenCanvas = new Canvas(mOffscreenBuffer); + } + } + onDrawKeyboard(mOffscreenCanvas); } - canvas.drawBitmap(mBuffer, 0, 0, null); + canvas.drawBitmap(mOffscreenBuffer, 0, 0, null); } - private void onBufferDraw() { + private boolean maybeAllocateOffscreenBuffer() { final int width = getWidth(); final int height = getHeight(); - if (width == 0 || height == 0) - return; - if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) { - if (mBuffer != null) - mBuffer.recycle(); - mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - mInvalidateAllKeys = true; - if (mCanvas != null) { - mCanvas.setBitmap(mBuffer); - } else { - mCanvas = new Canvas(mBuffer); - } + if (width == 0 || height == 0) { + return false; } + if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width + && mOffscreenBuffer.getHeight() == height) { + return false; + } + freeOffscreenBuffer(); + mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + return true; + } + private void freeOffscreenBuffer() { + if (mOffscreenBuffer != null) { + mOffscreenBuffer.recycle(); + mOffscreenBuffer = null; + } + } + + private void onDrawKeyboard(final Canvas canvas) { if (mKeyboard == null) return; - final Canvas canvas = mCanvas; + final int width = getWidth(); + final int height = getHeight(); final Paint paint = mPaint; final KeyDrawParams params = mKeyDrawParams; - if (mInvalidateAllKeys || mInvalidatedKeys.isEmpty()) { - mInvalidatedKeysRect.set(0, 0, width, height); - canvas.clipRect(mInvalidatedKeysRect, Op.REPLACE); - canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); + // Calculate clip region and set. + final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty(); + final boolean isHardwareAccelerated = canvas.isHardwareAccelerated(); + // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. + if (drawAllKeys || isHardwareAccelerated) { + mClipRegion.set(0, 0, width, height); + } else { + mClipRegion.setEmpty(); + for (final Key key : mInvalidatedKeys) { + if (mKeyboard.hasKey(key)) { + final int x = key.mX + getPaddingLeft(); + final int y = key.mY + getPaddingTop(); + mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight); + mClipRegion.union(mWorkingRect); + } + } + } + if (!isHardwareAccelerated) { + canvas.clipRegion(mClipRegion, Region.Op.REPLACE); + } + + // Draw keyboard background. + canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); + final Drawable background = getBackground(); + if (background != null) { + background.draw(canvas); + } + + // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. + if (drawAllKeys || isHardwareAccelerated) { // Draw all keys. for (final Key key : mKeyboard.mKeys) { onDrawKey(key, canvas, paint, params); } - if (mNeedsToDimEntireKeyboard) { - drawDimRectangle(canvas, mInvalidatedKeysRect, mBackgroundDimAlpha, paint); - } } else { // Draw invalidated keys. for (final Key key : mInvalidatedKeys) { - if (!mKeyboard.hasKey(key)) { - continue; - } - final int x = key.mX + getPaddingLeft(); - final int y = key.mY + getPaddingTop(); - mInvalidatedKeysRect.set(x, y, x + key.mWidth, y + key.mHeight); - canvas.clipRect(mInvalidatedKeysRect, Op.REPLACE); - canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); - onDrawKey(key, canvas, paint, params); - if (mNeedsToDimEntireKeyboard) { - drawDimRectangle(canvas, mInvalidatedKeysRect, mBackgroundDimAlpha, paint); + if (mKeyboard.hasKey(key)) { + onDrawKey(key, canvas, paint, params); } } } + // Overlay a dark rectangle to dim. + if (mNeedsToDimEntireKeyboard) { + paint.setColor(Color.BLACK); + paint.setAlpha(mBackgroundDimAlpha); + // Note: clipRegion() above is in effect if it was called. + canvas.drawRect(0, 0, width, height, paint); + } + // ResearchLogging indicator. // TODO: Reimplement using a keyboard background image specific to the ResearchLogger, // and remove this call. @@ -528,7 +568,6 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { } mInvalidatedKeys.clear(); - mInvalidatedKeysRect.setEmpty(); mInvalidateAllKeys = false; } @@ -853,13 +892,6 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { canvas.translate(-x, -y); } - // Overlay a dark rectangle to dim. - private static void drawDimRectangle(Canvas canvas, Rect rect, int alpha, Paint paint) { - paint.setColor(Color.BLACK); - paint.setAlpha(alpha); - canvas.drawRect(rect, paint); - } - public Paint newDefaultLabelPaint() { final Paint paint = new Paint(); paint.setAntiAlias(true); @@ -888,58 +920,33 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker); } - private static class PreviewView extends RelativeLayout { - KeyPreviewDrawParams mParams; - Paint mGesturePaint; - - public PreviewView(Context context, KeyPreviewDrawParams params, Paint gesturePaint) { - super(context); - setWillNotDraw(false); - mParams = params; - mGesturePaint = gesturePaint; - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - canvas.translate(mParams.mCoordinates[0], mParams.mCoordinates[1]); - PointerTracker.drawGestureTrailForAllPointerTrackers(canvas, mGesturePaint); - canvas.translate(-mParams.mCoordinates[0], -mParams.mCoordinates[1]); - } - } - private void addKeyPreview(TextView keyPreview) { - if (mPreviewPlacer == null) { - createPreviewPlacer(); - } - mPreviewPlacer.addView( - keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacer, 0, 0)); + locatePreviewPlacerView(); + mPreviewPlacerView.addView( + keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0)); } - private void createPreviewPlacer() { - mPreviewPlacer = new PreviewView(getContext(), mKeyPreviewDrawParams, mGesturePaint); + private void locatePreviewPlacerView() { + if (mPreviewPlacerView.getParent() != null) { + return; + } + final int[] viewOrigin = new int[2]; + getLocationInWindow(viewOrigin); + mPreviewPlacerView.setOrigin(viewOrigin[0], viewOrigin[1]); final ViewGroup windowContentView = (ViewGroup)getRootView().findViewById(android.R.id.content); - windowContentView.addView(mPreviewPlacer); + windowContentView.addView(mPreviewPlacerView); + } + + public void showGestureFloatingPreviewText(String gestureFloatingPreviewText) { + locatePreviewPlacerView(); + mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText); } @Override public void showGestureTrail(PointerTracker tracker) { - if (mPreviewPlacer == null) { - createPreviewPlacer(); - } - final Rect r = tracker.getBoundingBox(); - if (!r.isEmpty()) { - // Invalidate the rectangular region encompassing the gesture. This is needed because - // past points along the gesture will fade and gradually disappear. - final KeyPreviewDrawParams params = mKeyPreviewDrawParams; - mInvalidatedGesturesRect.set(r); - mInvalidatedGesturesRect.offset(params.mCoordinates[0], params.mCoordinates[1]); - mInvalidatedGesturesRect.inset(-GESTURE_DRAWING_WIDTH, -GESTURE_DRAWING_WIDTH); - mPreviewPlacer.invalidate(mInvalidatedGesturesRect); - } else { - mPreviewPlacer.invalidate(); - } + locatePreviewPlacerView(); + mPreviewPlacerView.invalidatePointer(tracker); } @SuppressWarnings("deprecation") // setBackgroundDrawable is replaced by setBackground in API16 @@ -1055,9 +1062,9 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { mInvalidatedKeys.add(key); final int x = key.mX + getPaddingLeft(); final int y = key.mY + getPaddingTop(); - mInvalidatedKeysRect.union(x, y, x + key.mWidth, y + key.mHeight); + mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight); mBufferNeedsUpdate = true; - invalidate(mInvalidatedKeysRect); + invalidate(mWorkingRect); } public void closing() { @@ -1082,12 +1089,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); closing(); - if (mPreviewPlacer != null) { - mPreviewPlacer.removeAllViews(); - } - if (mBuffer != null) { - mBuffer.recycle(); - mBuffer = null; - } + mPreviewPlacerView.removeAllViews(); + freeOffscreenBuffer(); } } diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 8c234e4e6..79459083f 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -43,6 +43,7 @@ import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy; import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; +import com.android.inputmethod.keyboard.internal.SuddenJumpingTouchEventHandler; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.LatinIME; import com.android.inputmethod.latin.LatinImeLogger; @@ -153,8 +154,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } break; case MSG_TYPING_STATE_EXPIRED: - cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator, - keyboardView.mAltCodeKeyWhileTypingFadeinAnimator); + startWhileTypingFadeinAnimation(keyboardView); break; } } @@ -228,7 +228,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key removeMessages(MSG_LONGPRESS_KEY); } - public static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel, + private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel, final ObjectAnimator animatorToStart) { float startFraction = 0.0f; if (animatorToCancel.isStarted()) { @@ -240,18 +240,39 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key animatorToStart.setCurrentPlayTime(startTime); } + private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) { + cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator, + keyboardView.mAltCodeKeyWhileTypingFadeinAnimator); + } + + private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) { + cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator, + keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator); + } + @Override - public void startTypingStateTimer() { + public void startTypingStateTimer(Key typedKey) { + if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) { + return; + } + final boolean isTyping = isTypingState(); removeMessages(MSG_TYPING_STATE_EXPIRED); + final MainKeyboardView keyboardView = getOuterInstance(); + + // When user hits the space or the enter key, just cancel the while-typing timer. + final int typedCode = typedKey.mCode; + if (typedCode == Keyboard.CODE_SPACE || typedCode == Keyboard.CODE_ENTER) { + startWhileTypingFadeinAnimation(keyboardView); + return; + } + sendMessageDelayed( obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout); if (isTyping) { return; } - final MainKeyboardView keyboardView = getOuterInstance(); - cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator, - keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator); + startWhileTypingFadeoutAnimation(keyboardView); } @Override @@ -461,7 +482,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key super.setKeyboard(keyboard); mKeyDetector.setKeyboard( keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection); - PointerTracker.setKeyDetector(mKeyDetector, mGestureInputEnabled); + PointerTracker.setKeyDetector(mKeyDetector, mShouldHandleGesture); mTouchScreenRegulator.setKeyboard(keyboard); mMoreKeysPanelCache.clear(); @@ -479,6 +500,14 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key AccessibleKeyboardViewProxy.getInstance().setKeyboard(keyboard); } + @Override + public void setGestureHandlingMode(final boolean shouldHandleGesture, + boolean drawsGesturePreviewTrail, boolean drawsGestureFloatingPreviewText) { + super.setGestureHandlingMode(shouldHandleGesture, drawsGesturePreviewTrail, + drawsGestureFloatingPreviewText); + PointerTracker.setKeyDetector(mKeyDetector, shouldHandleGesture); + } + /** * Returns whether the device has distinct multi-touch panel. * @return true if the device has distinct multi-touch panel. @@ -491,23 +520,6 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key mHasDistinctMultitouch = hasDistinctMultitouch; } - /** - * 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(); - } - @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java index cd4e3001e..a183546dd 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java @@ -39,7 +39,11 @@ public class MoreKeysDetector extends KeyDetector { Key nearestKey = null; int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare; - for (final Key key : getKeyboard().mKeys) { + final Keyboard keyboard = getKeyboard(); + if (keyboard == null) { + throw new NullPointerException("Keyboard isn't set"); + } + for (final Key key : keyboard.mKeys) { final int dist = key.squaredDistanceToEdge(touchX, touchY); if (dist < nearestDist) { nearestKey = key; diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index 4a5ecf986..e7e11f481 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -18,7 +18,6 @@ package com.android.inputmethod.keyboard; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Rect; import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; @@ -42,8 +41,8 @@ public class PointerTracker { private static final boolean DEBUG_LISTENER = false; private static boolean DEBUG_MODE = LatinImeLogger.sDBG; - // TODO: There should be an option to turn on/off the gesture input. - private static boolean sIsGestureEnabled = true; + /** True if {@link PointerTracker}s should handle gesture events. */ + private static boolean sShouldHandleGesture = false; private static final int MIN_GESTURE_RECOGNITION_TIME = 100; // msec @@ -83,7 +82,7 @@ public class PointerTracker { } public interface TimerProxy { - public void startTypingStateTimer(); + public void startTypingStateTimer(Key typedKey); public boolean isTypingState(); public void startKeyRepeatTimer(PointerTracker tracker); public void startLongPressTimer(PointerTracker tracker); @@ -96,7 +95,7 @@ public class PointerTracker { public static class Adapter implements TimerProxy { @Override - public void startTypingStateTimer() {} + public void startTypingStateTimer(Key typedKey) {} @Override public boolean isTypingState() { return false; } @Override @@ -199,7 +198,7 @@ public class PointerTracker { sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack; setParameters(MainKeyboardView.PointerTrackerParams.DEFAULT); - updateGestureInputEnabledState(null, false /* gestureInputEnabled */); + updateGestureHandlingMode(null, false /* shouldHandleGesture */); } public static void setParameters(MainKeyboardView.PointerTrackerParams params) { @@ -208,14 +207,13 @@ public class PointerTracker { params.mTouchNoiseThresholdDistance * params.mTouchNoiseThresholdDistance); } - private static void updateGestureInputEnabledState(Keyboard keyboard, - boolean gestureInputEnabled) { - if (!gestureInputEnabled + private static void updateGestureHandlingMode(Keyboard keyboard, boolean shouldHandleGesture) { + if (!shouldHandleGesture || AccessibilityUtils.getInstance().isTouchExplorationEnabled() || (keyboard != null && keyboard.mId.passwordInput())) { - sIsGestureEnabled = false; + sShouldHandleGesture = false; } else { - sIsGestureEnabled = true; + sShouldHandleGesture = true; } } @@ -243,7 +241,7 @@ public class PointerTracker { } } - public static void setKeyDetector(KeyDetector keyDetector, boolean gestureInputEnabledByUser) { + public static void setKeyDetector(KeyDetector keyDetector, boolean shouldHandleGesture) { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); @@ -252,7 +250,7 @@ public class PointerTracker { tracker.mKeyboardLayoutHasBeenChanged = true; } final Keyboard keyboard = keyDetector.getKeyboard(); - updateGestureInputEnabledState(keyboard, gestureInputEnabledByUser); + updateGestureHandlingMode(keyboard, shouldHandleGesture); } public static void dismissAllKeyPreviews() { @@ -297,17 +295,6 @@ public class PointerTracker { sAggregratedPointers.reset(); } - // TODO: To handle multi-touch gestures we may want to move this method to - // {@link PointerTrackerQueue}. - public static void drawGestureTrailForAllPointerTrackers(Canvas canvas, Paint paint) { - final int trackersSize = sTrackers.size(); - for (int i = 0; i < trackersSize; ++i) { - final PointerTracker tracker = sTrackers.get(i); - tracker.mGestureStroke.drawGestureTrail(canvas, paint, tracker.getLastX(), - tracker.getLastY()); - } - } - private PointerTracker(int id, KeyEventHandler handler) { if (handler == null) throw new NullPointerException(); @@ -342,9 +329,7 @@ public class PointerTracker { mListener.onPressKey(key.mCode); final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged; mKeyboardLayoutHasBeenChanged = false; - if (!key.altCodeWhileTyping() && !key.isModifier()) { - mTimerProxy.startTypingStateTimer(); - } + mTimerProxy.startTypingStateTimer(key); return keyboardLayoutHasBeenChanged; } return false; @@ -423,7 +408,7 @@ public class PointerTracker { if (mDrawingProxy != null) { setReleasedKeyGraphics(mCurrentKey); } - mCurrentKey = newKey; + // Keep {@link #mCurrentKey} that comes from previous keyboard. } final int keyQuarterWidth = mKeyboard.mMostCommonKeyWidth / 4; mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth; @@ -525,6 +510,12 @@ public class PointerTracker { mDrawingProxy.invalidateKey(key); } + public void drawGestureTrail(Canvas canvas, Paint paint) { + if (mInGesture) { + mGestureStroke.drawGestureTrail(canvas, paint, mLastX, mLastY); + } + } + public int getLastX() { return mLastX; } @@ -536,9 +527,6 @@ public class PointerTracker { public long getDownTime() { return mDownTime; } - public Rect getBoundingBox() { - return mGestureStroke.getBoundingBox(); - } private Key onDownKey(int x, int y, long eventTime) { mDownTime = eventTime; @@ -669,8 +657,8 @@ public class PointerTracker { if (queue != null && queue.size() == 1) { mIsPossibleGesture = false; // A gesture should start only from the letter key. - if (sIsGestureEnabled && mIsAlphabetKeyboard && !mIsShowingMoreKeysPanel && key != null - && Keyboard.isLetterCode(key.mCode)) { + if (sShouldHandleGesture && mIsAlphabetKeyboard && !mIsShowingMoreKeysPanel + && key != null && Keyboard.isLetterCode(key.mCode)) { mIsPossibleGesture = true; // TODO: pointer times should be relative to first down even in entire batch input // instead of resetting to 0 for each new down event. @@ -714,7 +702,7 @@ public class PointerTracker { private void onGestureMoveEvent(PointerTracker tracker, int x, int y, long eventTime, boolean isHistorical, Key key) { final int gestureTime = (int)(eventTime - tracker.getDownTime()); - if (sIsGestureEnabled && mIsPossibleGesture) { + if (sShouldHandleGesture && mIsPossibleGesture) { final GestureStroke stroke = mGestureStroke; stroke.addPoint(x, y, gestureTime, isHistorical); if (!mInGesture && stroke.isStartOfAGesture(gestureTime, sWasInGesture)) { @@ -804,13 +792,16 @@ public class PointerTracker { final int dx = x - lastX; final int dy = y - lastY; final int lastMoveSquared = dx * dx + dy * dy; + // TODO: Should find a way to balance gesture detection and this hack. if (sNeedsPhantomSuddenMoveEventHack - && lastMoveSquared >= mKeyQuarterWidthSquared) { + && lastMoveSquared >= mKeyQuarterWidthSquared + && !mIsPossibleGesture) { if (DEBUG_MODE) { Log.w(TAG, String.format("onMoveEvent:" + " phantom sudden move event is translated to " + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y)); } + // TODO: This should be moved to outside of this nested if-clause? if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY); } @@ -826,7 +817,9 @@ public class PointerTracker { && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) { onUpEventInternal(x, y, eventTime); } - mKeyAlreadyProcessed = true; + if (!mIsPossibleGesture) { + mKeyAlreadyProcessed = true; + } setReleasedKeyGraphics(oldKey); } } @@ -842,7 +835,9 @@ public class PointerTracker { if (mIsAllowedSlidingKeyInput) { onMoveToNewKey(key, x, y); } else { - mKeyAlreadyProcessed = true; + if (!mIsPossibleGesture) { + mKeyAlreadyProcessed = true; + } } } } @@ -881,6 +876,7 @@ public class PointerTracker { private void onUpEventInternal(int x, int y, long eventTime) { mTimerProxy.cancelKeyTimers(); mIsInSlidingKeyInput = false; + mIsPossibleGesture = false; // Release the last pressed key. setReleasedKeyGraphics(mCurrentKey); if (mIsShowingMoreKeysPanel) { @@ -958,9 +954,7 @@ public class PointerTracker { public void onRegisterKey(Key key) { if (key != null) { detectAndSendKey(key, key.mX, key.mY); - if (!key.altCodeWhileTyping() && !key.isModifier()) { - mTimerProxy.startTypingStateTimer(); - } + mTimerProxy.startTypingStateTimer(key); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java index c16b70ef0..28d6c1d07 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java @@ -16,7 +16,6 @@ package com.android.inputmethod.keyboard.internal; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Rect; import android.util.FloatMath; import com.android.inputmethod.latin.Constants; @@ -41,7 +40,6 @@ public class GestureStroke { private int mMinGestureLength; private int mMinGestureLengthWhileInGesture; private int mMinGestureSampleLength; - private final Rect mDrawingRect = new Rect(); // TODO: Move some of these to resource. private static final float MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH = 1.0f; @@ -90,7 +88,6 @@ public class GestureStroke { mEventTimes.setLength(0); mXCoordinates.setLength(0); mYCoordinates.setLength(0); - mDrawingRect.setEmpty(); } private void updateLastPoint(final int x, final int y, final int time) { @@ -198,8 +195,4 @@ public class GestureStroke { } } } - - public Rect getBoundingBox() { - return mDrawingRect; - } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index 53261205d..94a7b826f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -84,6 +84,19 @@ public class KeySpecParser { } mIconId = getIconId(moreKeySpec); } + + @Override + public String toString() { + final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel + : PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); + final String output = (mCode == Keyboard.CODE_OUTPUT_TEXT ? mOutputText + : Keyboard.printableCode(mCode)); + if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) { + return output; + } else { + return label + "|" + output; + } + } } private KeySpecParser() { diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index d732fc061..bec0f1fef 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -131,101 +131,102 @@ public final class KeyboardTextsSet { /* 23 */ "more_keys_for_nordic_row2_10", /* 24 */ "more_keys_for_nordic_row2_11", /* 25 */ "keylabel_for_east_slavic_row1_9", - /* 26 */ "keylabel_for_east_slavic_row2_1", - /* 27 */ "keylabel_for_east_slavic_row3_5", - /* 28 */ "more_keys_for_cyrillic_u", - /* 29 */ "more_keys_for_cyrillic_ye", - /* 30 */ "more_keys_for_cyrillic_en", - /* 31 */ "more_keys_for_cyrillic_ha", - /* 32 */ "more_keys_for_east_slavic_row2_1", - /* 33 */ "more_keys_for_cyrillic_o", - /* 34 */ "more_keys_for_cyrillic_soft_sign", - /* 35 */ "keylabel_for_south_slavic_row1_6", - /* 36 */ "keylabel_for_south_slavic_row2_11", - /* 37 */ "keylabel_for_south_slavic_row3_1", - /* 38 */ "keylabel_for_south_slavic_row3_8", - /* 39 */ "more_keys_for_cyrillic_ie", - /* 40 */ "more_keys_for_cyrillic_i", - /* 41 */ "more_keys_for_single_quote", - /* 42 */ "more_keys_for_double_quote", - /* 43 */ "more_keys_for_tablet_double_quote", - /* 44 */ "more_keys_for_currency_dollar", - /* 45 */ "more_keys_for_currency_euro", - /* 46 */ "more_keys_for_currency_pound", - /* 47 */ "more_keys_for_currency_general", - /* 48 */ "more_keys_for_punctuation", - /* 49 */ "more_keys_for_star", - /* 50 */ "more_keys_for_bullet", - /* 51 */ "more_keys_for_plus", - /* 52 */ "more_keys_for_left_parenthesis", - /* 53 */ "more_keys_for_right_parenthesis", - /* 54 */ "more_keys_for_less_than", - /* 55 */ "more_keys_for_greater_than", - /* 56 */ "more_keys_for_arabic_diacritics", - /* 57 */ "keyhintlabel_for_arabic_diacritics", - /* 58 */ "keylabel_for_symbols_1", - /* 59 */ "keylabel_for_symbols_2", - /* 60 */ "keylabel_for_symbols_3", - /* 61 */ "keylabel_for_symbols_4", - /* 62 */ "keylabel_for_symbols_5", - /* 63 */ "keylabel_for_symbols_6", - /* 64 */ "keylabel_for_symbols_7", - /* 65 */ "keylabel_for_symbols_8", - /* 66 */ "keylabel_for_symbols_9", - /* 67 */ "keylabel_for_symbols_0", - /* 68 */ "additional_more_keys_for_symbols_1", - /* 69 */ "additional_more_keys_for_symbols_2", - /* 70 */ "additional_more_keys_for_symbols_3", - /* 71 */ "additional_more_keys_for_symbols_4", - /* 72 */ "additional_more_keys_for_symbols_5", - /* 73 */ "additional_more_keys_for_symbols_6", - /* 74 */ "additional_more_keys_for_symbols_7", - /* 75 */ "additional_more_keys_for_symbols_8", - /* 76 */ "additional_more_keys_for_symbols_9", - /* 77 */ "additional_more_keys_for_symbols_0", - /* 78 */ "more_keys_for_symbols_1", - /* 79 */ "more_keys_for_symbols_2", - /* 80 */ "more_keys_for_symbols_3", - /* 81 */ "more_keys_for_symbols_4", - /* 82 */ "more_keys_for_symbols_5", - /* 83 */ "more_keys_for_symbols_6", - /* 84 */ "more_keys_for_symbols_7", - /* 85 */ "more_keys_for_symbols_8", - /* 86 */ "more_keys_for_symbols_9", - /* 87 */ "more_keys_for_symbols_0", - /* 88 */ "keylabel_for_comma", - /* 89 */ "more_keys_for_comma", - /* 90 */ "keylabel_for_symbols_question", - /* 91 */ "keylabel_for_symbols_semicolon", - /* 92 */ "keylabel_for_symbols_percent", - /* 93 */ "more_keys_for_symbols_exclamation", - /* 94 */ "more_keys_for_symbols_question", - /* 95 */ "more_keys_for_symbols_semicolon", - /* 96 */ "more_keys_for_symbols_percent", - /* 97 */ "keylabel_for_tablet_comma", - /* 98 */ "keyhintlabel_for_tablet_comma", - /* 99 */ "more_keys_for_tablet_comma", - /* 100 */ "keyhintlabel_for_tablet_period", - /* 101 */ "more_keys_for_tablet_period", - /* 102 */ "keylabel_for_apostrophe", - /* 103 */ "keyhintlabel_for_apostrophe", - /* 104 */ "more_keys_for_apostrophe", - /* 105 */ "more_keys_for_am_pm", - /* 106 */ "settings_as_more_key", - /* 107 */ "shortcut_as_more_key", - /* 108 */ "action_next_as_more_key", - /* 109 */ "action_previous_as_more_key", - /* 110 */ "label_to_more_symbol_key", - /* 111 */ "label_to_more_symbol_for_tablet_key", - /* 112 */ "label_tab_key", - /* 113 */ "label_to_phone_numeric_key", - /* 114 */ "label_to_phone_symbols_key", - /* 115 */ "label_time_am", - /* 116 */ "label_time_pm", - /* 117 */ "label_to_symbol_key_pcqwerty", - /* 118 */ "keylabel_for_popular_domain", - /* 119 */ "more_keys_for_popular_domain", - /* 120 */ "more_keys_for_smiley", + /* 26 */ "keylabel_for_east_slavic_row1_12", + /* 27 */ "keylabel_for_east_slavic_row2_1", + /* 28 */ "keylabel_for_east_slavic_row2_11", + /* 29 */ "keylabel_for_east_slavic_row3_5", + /* 30 */ "more_keys_for_cyrillic_u", + /* 31 */ "more_keys_for_cyrillic_en", + /* 32 */ "more_keys_for_cyrillic_ghe", + /* 33 */ "more_keys_for_east_slavic_row2_1", + /* 34 */ "more_keys_for_cyrillic_o", + /* 35 */ "more_keys_for_cyrillic_soft_sign", + /* 36 */ "keylabel_for_south_slavic_row1_6", + /* 37 */ "keylabel_for_south_slavic_row2_11", + /* 38 */ "keylabel_for_south_slavic_row3_1", + /* 39 */ "keylabel_for_south_slavic_row3_8", + /* 40 */ "more_keys_for_cyrillic_ie", + /* 41 */ "more_keys_for_cyrillic_i", + /* 42 */ "more_keys_for_single_quote", + /* 43 */ "more_keys_for_double_quote", + /* 44 */ "more_keys_for_tablet_double_quote", + /* 45 */ "more_keys_for_currency_dollar", + /* 46 */ "more_keys_for_currency_euro", + /* 47 */ "more_keys_for_currency_pound", + /* 48 */ "more_keys_for_currency_general", + /* 49 */ "more_keys_for_punctuation", + /* 50 */ "more_keys_for_star", + /* 51 */ "more_keys_for_bullet", + /* 52 */ "more_keys_for_plus", + /* 53 */ "more_keys_for_left_parenthesis", + /* 54 */ "more_keys_for_right_parenthesis", + /* 55 */ "more_keys_for_less_than", + /* 56 */ "more_keys_for_greater_than", + /* 57 */ "more_keys_for_arabic_diacritics", + /* 58 */ "keyhintlabel_for_arabic_diacritics", + /* 59 */ "keylabel_for_symbols_1", + /* 60 */ "keylabel_for_symbols_2", + /* 61 */ "keylabel_for_symbols_3", + /* 62 */ "keylabel_for_symbols_4", + /* 63 */ "keylabel_for_symbols_5", + /* 64 */ "keylabel_for_symbols_6", + /* 65 */ "keylabel_for_symbols_7", + /* 66 */ "keylabel_for_symbols_8", + /* 67 */ "keylabel_for_symbols_9", + /* 68 */ "keylabel_for_symbols_0", + /* 69 */ "additional_more_keys_for_symbols_1", + /* 70 */ "additional_more_keys_for_symbols_2", + /* 71 */ "additional_more_keys_for_symbols_3", + /* 72 */ "additional_more_keys_for_symbols_4", + /* 73 */ "additional_more_keys_for_symbols_5", + /* 74 */ "additional_more_keys_for_symbols_6", + /* 75 */ "additional_more_keys_for_symbols_7", + /* 76 */ "additional_more_keys_for_symbols_8", + /* 77 */ "additional_more_keys_for_symbols_9", + /* 78 */ "additional_more_keys_for_symbols_0", + /* 79 */ "more_keys_for_symbols_1", + /* 80 */ "more_keys_for_symbols_2", + /* 81 */ "more_keys_for_symbols_3", + /* 82 */ "more_keys_for_symbols_4", + /* 83 */ "more_keys_for_symbols_5", + /* 84 */ "more_keys_for_symbols_6", + /* 85 */ "more_keys_for_symbols_7", + /* 86 */ "more_keys_for_symbols_8", + /* 87 */ "more_keys_for_symbols_9", + /* 88 */ "more_keys_for_symbols_0", + /* 89 */ "keylabel_for_comma", + /* 90 */ "more_keys_for_comma", + /* 91 */ "keylabel_for_symbols_question", + /* 92 */ "keylabel_for_symbols_semicolon", + /* 93 */ "keylabel_for_symbols_percent", + /* 94 */ "more_keys_for_symbols_exclamation", + /* 95 */ "more_keys_for_symbols_question", + /* 96 */ "more_keys_for_symbols_semicolon", + /* 97 */ "more_keys_for_symbols_percent", + /* 98 */ "keylabel_for_tablet_comma", + /* 99 */ "keyhintlabel_for_tablet_comma", + /* 100 */ "more_keys_for_tablet_comma", + /* 101 */ "keyhintlabel_for_tablet_period", + /* 102 */ "more_keys_for_tablet_period", + /* 103 */ "keylabel_for_apostrophe", + /* 104 */ "keyhintlabel_for_apostrophe", + /* 105 */ "more_keys_for_apostrophe", + /* 106 */ "more_keys_for_am_pm", + /* 107 */ "settings_as_more_key", + /* 108 */ "shortcut_as_more_key", + /* 109 */ "action_next_as_more_key", + /* 110 */ "action_previous_as_more_key", + /* 111 */ "label_to_more_symbol_key", + /* 112 */ "label_to_more_symbol_for_tablet_key", + /* 113 */ "label_tab_key", + /* 114 */ "label_to_phone_numeric_key", + /* 115 */ "label_to_phone_symbols_key", + /* 116 */ "label_time_am", + /* 117 */ "label_time_pm", + /* 118 */ "label_to_symbol_key_pcqwerty", + /* 119 */ "keylabel_for_popular_domain", + /* 120 */ "more_keys_for_popular_domain", + /* 121 */ "more_keys_for_smiley", }; private static final String EMPTY = ""; @@ -236,41 +237,41 @@ public final class KeyboardTextsSet { EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - EMPTY, EMPTY, - /* ~40 */ - /* 41 */ "!fixedColumnOrder!4,\u2018,\u2019,\u201A,\u201B", + EMPTY, EMPTY, EMPTY, + /* ~41 */ + /* 42 */ "!fixedColumnOrder!4,\u2018,\u2019,\u201A,\u201B", // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,“,”,„,‟,«,»</string> - /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB", + /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB", // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«,»,‘,’,‚,‛</string> - /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", + /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", // U+00A2: "¢" CENT SIGN // U+00A3: "£" POUND SIGN // U+20AC: "€" EURO SIGN // U+00A5: "¥" YEN SIGN // U+20B1: "₱" PESO SIGN - /* 44 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", - /* 45 */ "\u00A2,\u00A3,$,\u00A5,\u20B1", - /* 46 */ "\u00A2,$,\u20AC,\u00A5,\u20B1", - /* 47 */ "\u00A2,$,\u20AC,\u00A3,\u00A5,\u20B1", - /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)", + /* 45 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", + /* 46 */ "\u00A2,\u00A3,$,\u00A5,\u20B1", + /* 47 */ "\u00A2,$,\u20AC,\u00A5,\u20B1", + /* 48 */ "\u00A2,$,\u20AC,\u00A3,\u00A5,\u20B1", + /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)", // U+2020: "†" DAGGER // U+2021: "‡" DOUBLE DAGGER // U+2605: "★" BLACK STAR - /* 49 */ "\u2020,\u2021,\u2605", + /* 50 */ "\u2020,\u2021,\u2605", // U+266A: "♪" EIGHTH NOTE // U+2665: "♥" BLACK HEART SUIT // U+2660: "♠" BLACK SPADE SUIT // U+2666: "♦" BLACK DIAMOND SUIT // U+2663: "♣" BLACK CLUB SUIT - /* 50 */ "\u266A,\u2665,\u2660,\u2666,\u2663", + /* 51 */ "\u266A,\u2665,\u2660,\u2666,\u2663", // U+00B1: "±" PLUS-MINUS SIGN - /* 51 */ "\u00B1", + /* 52 */ "\u00B1", // The all letters need to be mirrored are found at // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - /* 52 */ "!fixedColumnOrder!3,<,{,[", - /* 53 */ "!fixedColumnOrder!3,>,},]", + /* 53 */ "!fixedColumnOrder!3,<,{,[", + /* 54 */ "!fixedColumnOrder!3,>,},]", // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK // U+2264: "≤" LESS-THAN OR EQUAL TO @@ -286,95 +287,148 @@ public final class KeyboardTextsSet { // U+201D: "”" RIGHT DOUBLE QUOTATION MARK // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - /* 54 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB", - /* 55 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB", - /* 56 */ EMPTY, + /* 55 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB", + /* 56 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB", /* 57 */ EMPTY, - /* 58 */ "1", - /* 59 */ "2", - /* 60 */ "3", - /* 61 */ "4", - /* 62 */ "5", - /* 63 */ "6", - /* 64 */ "7", - /* 65 */ "8", - /* 66 */ "9", - /* 67 */ "0", - /* 68~ */ + /* 58 */ EMPTY, + /* 59 */ "1", + /* 60 */ "2", + /* 61 */ "3", + /* 62 */ "4", + /* 63 */ "5", + /* 64 */ "6", + /* 65 */ "7", + /* 66 */ "8", + /* 67 */ "9", + /* 68 */ "0", + /* 69~ */ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - /* ~77 */ + /* ~78 */ // U+00B9: "¹" SUPERSCRIPT ONE // U+00BD: "½" VULGAR FRACTION ONE HALF // U+2153: "⅓" VULGAR FRACTION ONE THIRD // U+00BC: "¼" VULGAR FRACTION ONE QUARTER // U+215B: "⅛" VULGAR FRACTION ONE EIGHTH - /* 78 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B", + /* 79 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B", // U+00B2: "²" SUPERSCRIPT TWO // U+2154: "⅔" VULGAR FRACTION TWO THIRDS - /* 79 */ "\u00B2,\u2154", + /* 80 */ "\u00B2,\u2154", // U+00B3: "³" SUPERSCRIPT THREE // U+00BE: "¾" VULGAR FRACTION THREE QUARTERS // U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS - /* 80 */ "\u00B3,\u00BE,\u215C", + /* 81 */ "\u00B3,\u00BE,\u215C", // U+2074: "⁴" SUPERSCRIPT FOUR - /* 81 */ "\u2074", + /* 82 */ "\u2074", // U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS - /* 82 */ "\u215D", - /* 83 */ EMPTY, + /* 83 */ "\u215D", + /* 84 */ EMPTY, // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS - /* 84 */ "\u215E", - /* 85 */ EMPTY, + /* 85 */ "\u215E", /* 86 */ EMPTY, + /* 87 */ EMPTY, // U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N // U+2205: "∅" EMPTY SET - /* 87 */ "\u207F,\u2205", - /* 88 */ ",", - /* 89 */ EMPTY, - /* 90 */ "?", - /* 91 */ ";", - /* 92 */ "%", + /* 88 */ "\u207F,\u2205", + /* 89 */ ",", + /* 90 */ EMPTY, + /* 91 */ "?", + /* 92 */ ";", + /* 93 */ "%", // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 93 */ "\u00A1", + /* 94 */ "\u00A1", // U+00BF: "¿" INVERTED QUESTION MARK - /* 94 */ "\u00BF", - /* 95 */ EMPTY, + /* 95 */ "\u00BF", + /* 96 */ EMPTY, // U+2030: "‰" PER MILLE SIGN - /* 96 */ "\u2030", - /* 97 */ ",", - /* 98 */ "!", + /* 97 */ "\u2030", + /* 98 */ ",", /* 99 */ "!", - /* 100 */ "?", + /* 100 */ "!", /* 101 */ "?", - /* 102 */ "\'", - /* 103 */ "\"", + /* 102 */ "?", + /* 103 */ "\'", /* 104 */ "\"", - /* 105 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm", - /* 106 */ "!icon/settings_key|!code/key_settings", - /* 107 */ "!icon/shortcut_key|!code/key_shortcut", - /* 108 */ "!hasLabels!,!text/label_next_key|!code/key_action_next", - /* 109 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", + /* 105 */ "\"", + /* 106 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm", + /* 107 */ "!icon/settings_key|!code/key_settings", + /* 108 */ "!icon/shortcut_key|!code/key_shortcut", + /* 109 */ "!hasLabels!,!text/label_next_key|!code/key_action_next", + /* 110 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", // Label for "switch to more symbol" modifier key. Must be short to fit on key! - /* 110 */ "= \\ <", + /* 111 */ "= \\ <", // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key! - /* 111 */ "~ \\ {", + /* 112 */ "~ \\ {", // Label for "Tab" key. Must be short to fit on key! - /* 112 */ "Tab", + /* 113 */ "Tab", // Label for "switch to phone numeric" key. Must be short to fit on key! - /* 113 */ "123", + /* 114 */ "123", // Label for "switch to phone symbols" key. Must be short to fit on key! // U+FF0A: "*" FULLWIDTH ASTERISK // U+FF03: "#" FULLWIDTH NUMBER SIGN - /* 114 */ "\uFF0A\uFF03", + /* 115 */ "\uFF0A\uFF03", // Key label for "ante meridiem" - /* 115 */ "AM", + /* 116 */ "AM", // Key label for "post meridiem" - /* 116 */ "PM", + /* 117 */ "PM", // Label for "switch to symbols" key on PC QWERTY layout - /* 117 */ "Sym", - /* 118 */ ".com", + /* 118 */ "Sym", + /* 119 */ ".com", // popular web domains for the locale - most popular, displayed on the keyboard - /* 119 */ "!hasLabels!,.net,.org,.gov,.edu", - /* 120 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ", + /* 120 */ "!hasLabels!,.net,.org,.gov,.edu", + /* 121 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ", + }; + + /* Language af: Afrikaans */ + private static final String[] LANGUAGE_af = { + // This is the same as Dutch except more keys of y and demoting vowels with diaeresis. + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* 0 */ "\u00E1,\u00E2,\u00E4,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* 2 */ "\u00ED,\u00EC,\u00EF,\u00EE,\u012F,\u012B,\u0133", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* 3 */ "\u00F3,\u00F4,\u00F6,\u00F2,\u00F5,\u0153,\u00F8,\u014D", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* 4 */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", + /* 5 */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* 6 */ "\u00F1,\u0144", + /* 7 */ null, + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* 8 */ "\u00FD,\u0133", }; /* Language ar: Arabic */ @@ -382,33 +436,33 @@ public final class KeyboardTextsSet { /* 0~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, - /* ~41 */ + null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK // <string name="more_keys_for_double_quote">“,”,„,‟,«|»,»|«</string> - /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB", + /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB", // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</string> - /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", - /* 44~ */ + /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", + /* 45~ */ null, null, null, null, - /* ~47 */ + /* ~48 */ // U+061F: "؟" ARABIC QUESTION MARK // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON - /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)", + /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)", // U+2605: "★" BLACK STAR // U+066D: "٭" ARABIC FIVE POINTED STAR - /* 49 */ "\u2605,\u066D", + /* 50 */ "\u2605,\u066D", // U+266A: "♪" EIGHTH NOTE - /* 50 */ "\u266A", - /* 51 */ null, + /* 51 */ "\u266A", + /* 52 */ null, // The all letters need to be mirrored are found at // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS - /* 52 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", - /* 53 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", + /* 53 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", + /* 54 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", // U+2264: "≤" LESS-THAN OR EQUAL TO // U+2265: "≥" GREATER-THAN EQUAL TO // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK @@ -424,8 +478,8 @@ public final class KeyboardTextsSet { // U+201D: "”" RIGHT DOUBLE QUOTATION MARK // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", - /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", + /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", + /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", // U+0655: "ٕ" ARABIC HAMZA BELOW // U+0654: "ٔ" ARABIC HAMZA ABOVE // U+0652: "ْ" ARABIC SUKUN @@ -441,64 +495,64 @@ public final class KeyboardTextsSet { // U+064E: "َ" ARABIC FATHA // U+0640: "ـ" ARABIC TATWEEL // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. - /* 56 */ "!fixedColumnOrder!7,\u0655,\u0654,\u0652,\u064D,\u064C,\u064B,\u0651,\u0656,\u0670,\u0653,\u0650,\u064F,\u064E,\u0640\u0640\u0640|\u0640", - /* 57 */ "\u0651", + /* 57 */ "!fixedColumnOrder!7,\u0655,\u0654,\u0652,\u064D,\u064C,\u064B,\u0651,\u0656,\u0670,\u0653,\u0650,\u064F,\u064E,\u0640\u0640\u0640|\u0640", + /* 58 */ "\u0651", // U+0661: "١" ARABIC-INDIC DIGIT ONE - /* 58 */ "\u0661", + /* 59 */ "\u0661", // U+0662: "٢" ARABIC-INDIC DIGIT TWO - /* 59 */ "\u0662", + /* 60 */ "\u0662", // U+0663: "٣" ARABIC-INDIC DIGIT THREE - /* 60 */ "\u0663", + /* 61 */ "\u0663", // U+0664: "٤" ARABIC-INDIC DIGIT FOUR - /* 61 */ "\u0664", + /* 62 */ "\u0664", // U+0665: "٥" ARABIC-INDIC DIGIT FIVE - /* 62 */ "\u0665", + /* 63 */ "\u0665", // U+0666: "٦" ARABIC-INDIC DIGIT SIX - /* 63 */ "\u0666", + /* 64 */ "\u0666", // U+0667: "٧" ARABIC-INDIC DIGIT SEVEN - /* 64 */ "\u0667", + /* 65 */ "\u0667", // U+0668: "٨" ARABIC-INDIC DIGIT EIGHT - /* 65 */ "\u0668", + /* 66 */ "\u0668", // U+0669: "٩" ARABIC-INDIC DIGIT NINE - /* 66 */ "\u0669", + /* 67 */ "\u0669", // U+0660: "٠" ARABIC-INDIC DIGIT ZERO - /* 67 */ "\u0660", - /* 68 */ "1", - /* 69 */ "2", - /* 70 */ "3", - /* 71 */ "4", - /* 72 */ "5", - /* 73 */ "6", - /* 74 */ "7", - /* 75 */ "8", - /* 76 */ "9", + /* 68 */ "\u0660", + /* 69 */ "1", + /* 70 */ "2", + /* 71 */ "3", + /* 72 */ "4", + /* 73 */ "5", + /* 74 */ "6", + /* 75 */ "7", + /* 76 */ "8", + /* 77 */ "9", // U+066B: "٫" ARABIC DECIMAL SEPARATOR // U+066C: "٬" ARABIC THOUSANDS SEPARATOR - /* 77 */ "0,\u066B,\u066C", - /* 78~ */ + /* 78 */ "0,\u066B,\u066C", + /* 79~ */ null, null, null, null, null, null, null, null, null, null, - /* ~87 */ + /* ~88 */ // U+060C: "،" ARABIC COMMA - /* 88 */ "\u060C", - /* 89 */ "\\,", - /* 90 */ "\u061F", - /* 91 */ "\u061B", + /* 89 */ "\u060C", + /* 90 */ "\\,", + /* 91 */ "\u061F", + /* 92 */ "\u061B", // U+066A: "٪" ARABIC PERCENT SIGN - /* 92 */ "\u066A", - /* 93 */ null, - /* 94 */ "?", - /* 95 */ ";", + /* 93 */ "\u066A", + /* 94 */ null, + /* 95 */ "?", + /* 96 */ ";", // U+2030: "‰" PER MILLE SIGN - /* 96 */ "\\%,\u2030", - /* 97~ */ + /* 97 */ "\\%,\u2030", + /* 98~ */ null, null, null, null, null, - /* ~101 */ + /* ~102 */ // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON // U+061F: "؟" ARABIC QUESTION MARK - /* 102 */ "\u060C", - /* 103 */ "\u061F", - /* 104 */ "\u061F,\u061B,!,:,-,/,\',\"", + /* 103 */ "\u060C", + /* 104 */ "\u061F", + /* 105 */ "\u061F,\u061B,!,:,-,/,\',\"", }; /* Language be: Belarusian */ @@ -509,19 +563,24 @@ public final class KeyboardTextsSet { /* ~24 */ // U+045E: "ў" CYRILLIC SMALL LETTER SHORT U /* 25 */ "\u045E", + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* 26 */ "\u0451", // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 26 */ "\u044B", + /* 27 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* 28 */ "\u044D", // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - /* 27 */ "\u0456", - /* 28~ */ - null, null, null, - /* ~30 */ - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 31 */ "\u044A", - /* 32 */ null, - /* 33 */ null, + /* 29 */ "\u0456", + /* 30~ */ + null, null, null, null, null, + /* ~34 */ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 34 */ "\u044A", + /* 35 */ "\u044A", + /* 36~ */ + null, null, null, null, + /* ~39 */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* 40 */ "\u0451", }; /* Language ca: Catalan */ @@ -854,22 +913,22 @@ public final class KeyboardTextsSet { /* 8~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~47 */ + null, null, null, null, null, null, null, null, null, null, null, + /* ~48 */ // U+00A1: "¡" INVERTED EXCLAMATION MARK // U+00BF: "¿" INVERTED QUESTION MARK - /* 48 */ "!fixedColumnOrder!9,\u00A1,\",\',#,-,:,!,\\,,?,\u00BF,@,&,\\%,+,;,/,(,)", - /* 49~ */ + /* 49 */ "!fixedColumnOrder!9,\u00A1,\",\',#,-,:,!,\\,,?,\u00BF,@,&,\\%,+,;,/,(,)", + /* 50~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~98 */ + /* ~99 */ // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 99 */ "!,\u00A1", - /* 100 */ null, + /* 100 */ "!,\u00A1", + /* 101 */ null, // U+00BF: "¿" INVERTED QUESTION MARK - /* 101 */ "?,\u00BF", + /* 102 */ "?,\u00BF", }; /* Language et: Estonian */ @@ -977,33 +1036,33 @@ public final class KeyboardTextsSet { /* 0~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, - /* ~41 */ + null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK // <string name="more_keys_for_double_quote">“,”,„,‟,«|»,»|«</string> - /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\",\'", + /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\",\'", // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</string> - /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", - /* 44~ */ + /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", + /* 45~ */ null, null, null, null, - /* ~47 */ + /* ~48 */ // U+061F: "؟" ARABIC QUESTION MARK // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON - /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)", + /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)", // U+2605: "★" BLACK STAR // U+066D: "٭" ARABIC FIVE POINTED STAR - /* 49 */ "\u2605,\u066D", + /* 50 */ "\u2605,\u066D", // U+266A: "♪" EIGHTH NOTE - /* 50 */ "\u266A", - /* 51 */ null, + /* 51 */ "\u266A", + /* 52 */ null, // The all letters need to be mirrored are found at // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS - /* 52 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", - /* 53 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", + /* 53 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", + /* 54 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", // U+2264: "≤" LESS-THAN OR EQUAL TO // U+2265: "≥" GREATER-THAN EQUAL TO // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK @@ -1019,8 +1078,8 @@ public final class KeyboardTextsSet { // U+201D: "”" RIGHT DOUBLE QUOTATION MARK // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>", - /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<", + /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>", + /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<", // U+0655: "ٕ" ARABIC HAMZA BELOW // U+0652: "ْ" ARABIC SUKUN // U+0651: "ّ" ARABIC SHADDA @@ -1036,68 +1095,68 @@ public final class KeyboardTextsSet { // U+064E: "َ" ARABIC FATHA // U+0640: "ـ" ARABIC TATWEEL // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. - /* 56 */ "!fixedColumnOrder!7,\u0655,\u0652,\u0651,\u064C,\u064D,\u064B,\u0654,\u0656,\u0670,\u0653,\u064F,\u0650,\u064E,\u0640\u0640\u0640|\u0640", - /* 57 */ "\u064B", + /* 57 */ "!fixedColumnOrder!7,\u0655,\u0652,\u0651,\u064C,\u064D,\u064B,\u0654,\u0656,\u0670,\u0653,\u064F,\u0650,\u064E,\u0640\u0640\u0640|\u0640", + /* 58 */ "\u064B", // U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE - /* 58 */ "\u06F1", + /* 59 */ "\u06F1", // U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO - /* 59 */ "\u06F2", + /* 60 */ "\u06F2", // U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE - /* 60 */ "\u06F3", + /* 61 */ "\u06F3", // U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR - /* 61 */ "\u06F4", + /* 62 */ "\u06F4", // U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE - /* 62 */ "\u06F5", + /* 63 */ "\u06F5", // U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX - /* 63 */ "\u06F6", + /* 64 */ "\u06F6", // U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN - /* 64 */ "\u06F7", + /* 65 */ "\u06F7", // U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT - /* 65 */ "\u06F8", + /* 66 */ "\u06F8", // U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE - /* 66 */ "\u06F9", + /* 67 */ "\u06F9", // U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO - /* 67 */ "\u06F0", - /* 68 */ "1", - /* 69 */ "2", - /* 70 */ "3", - /* 71 */ "4", - /* 72 */ "5", - /* 73 */ "6", - /* 74 */ "7", - /* 75 */ "8", - /* 76 */ "9", + /* 68 */ "\u06F0", + /* 69 */ "1", + /* 70 */ "2", + /* 71 */ "3", + /* 72 */ "4", + /* 73 */ "5", + /* 74 */ "6", + /* 75 */ "7", + /* 76 */ "8", + /* 77 */ "9", // U+066B: "٫" ARABIC DECIMAL SEPARATOR // U+066C: "٬" ARABIC THOUSANDS SEPARATOR - /* 77 */ "0,\u066B,\u066C", - /* 78~ */ + /* 78 */ "0,\u066B,\u066C", + /* 79~ */ null, null, null, null, null, null, null, null, null, null, - /* ~87 */ + /* ~88 */ // U+060C: "،" ARABIC COMMA - /* 88 */ "\u060C", - /* 89 */ "\\,", - /* 90 */ "\u061F", - /* 91 */ "\u061B", + /* 89 */ "\u060C", + /* 90 */ "\\,", + /* 91 */ "\u061F", + /* 92 */ "\u061B", // U+066A: "٪" ARABIC PERCENT SIGN - /* 92 */ "\u066A", - /* 93 */ null, - /* 94 */ "?", - /* 95 */ ";", + /* 93 */ "\u066A", + /* 94 */ null, + /* 95 */ "?", + /* 96 */ ";", // U+2030: "‰" PER MILLE SIGN - /* 96 */ "\\%,\u2030", + /* 97 */ "\\%,\u2030", // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON // U+061F: "؟" ARABIC QUESTION MARK // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - /* 97 */ "\u060C", - /* 98 */ "!", - /* 99 */ "!,\\,", - /* 100 */ "\u061F", - /* 101 */ "\u061F,?", - /* 102 */ "\u060C", - /* 103 */ "\u061F", - /* 104 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB", + /* 98 */ "\u060C", + /* 99 */ "!", + /* 100 */ "!,\\,", + /* 101 */ "\u061F", + /* 102 */ "\u061F,?", + /* 103 */ "\u060C", + /* 104 */ "\u061F", + /* 105 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB", }; /* Language fi: Finnish */ @@ -1206,38 +1265,38 @@ public final class KeyboardTextsSet { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~57 */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~58 */ // U+0967: "१" DEVANAGARI DIGIT ONE - /* 58 */ "\u0967", + /* 59 */ "\u0967", // U+0968: "२" DEVANAGARI DIGIT TWO - /* 59 */ "\u0968", + /* 60 */ "\u0968", // U+0969: "३" DEVANAGARI DIGIT THREE - /* 60 */ "\u0969", + /* 61 */ "\u0969", // U+096A: "४" DEVANAGARI DIGIT FOUR - /* 61 */ "\u096A", + /* 62 */ "\u096A", // U+096B: "५" DEVANAGARI DIGIT FIVE - /* 62 */ "\u096B", + /* 63 */ "\u096B", // U+096C: "६" DEVANAGARI DIGIT SIX - /* 63 */ "\u096C", + /* 64 */ "\u096C", // U+096D: "७" DEVANAGARI DIGIT SEVEN - /* 64 */ "\u096D", + /* 65 */ "\u096D", // U+096E: "८" DEVANAGARI DIGIT EIGHT - /* 65 */ "\u096E", + /* 66 */ "\u096E", // U+096F: "९" DEVANAGARI DIGIT NINE - /* 66 */ "\u096F", + /* 67 */ "\u096F", // U+0966: "०" DEVANAGARI DIGIT ZERO - /* 67 */ "\u0966", - /* 68 */ "1", - /* 69 */ "2", - /* 70 */ "3", - /* 71 */ "4", - /* 72 */ "5", - /* 73 */ "6", - /* 74 */ "7", - /* 75 */ "8", - /* 76 */ "9", - /* 77 */ "0", + /* 68 */ "\u0966", + /* 69 */ "1", + /* 70 */ "2", + /* 71 */ "3", + /* 72 */ "4", + /* 73 */ "5", + /* 74 */ "6", + /* 75 */ "7", + /* 76 */ "8", + /* 77 */ "9", + /* 78 */ "0", }; /* Language hr: Croatian */ @@ -1425,27 +1484,27 @@ public final class KeyboardTextsSet { /* 0~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, - /* ~41 */ + null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK // <string name="more_keys_for_double_quote">“,”,„,‟,«|»,»|«</string> - /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB", + /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB", // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</string> - /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", - /* 44~ */ + /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", + /* 45~ */ null, null, null, null, null, - /* ~48 */ + /* ~49 */ // U+2605: "★" BLACK STAR - /* 49 */ "\u2605", - /* 50 */ null, + /* 50 */ "\u2605", + /* 51 */ null, // U+00B1: "±" PLUS-MINUS SIGN // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN - /* 51 */ "\u00B1,\uFB29", + /* 52 */ "\u00B1,\uFB29", // The all letters need to be mirrored are found at // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - /* 52 */ "!fixedColumnOrder!3,<|>,{|},[|]", - /* 53 */ "!fixedColumnOrder!3,>|<,}|{,]|[", + /* 53 */ "!fixedColumnOrder!3,<|>,{|},[|]", + /* 54 */ "!fixedColumnOrder!3,>|<,}|{,]|[", // U+2264: "≤" LESS-THAN OR EQUAL TO // U+2265: "≥" GREATER-THAN EQUAL TO // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK @@ -1461,8 +1520,8 @@ public final class KeyboardTextsSet { // U+201D: "”" RIGHT DOUBLE QUOTATION MARK // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", - /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", + /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", + /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", }; /* Language ky: Kirghiz */ @@ -1473,22 +1532,29 @@ public final class KeyboardTextsSet { /* ~24 */ // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA /* 25 */ "\u0449", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* 26 */ "\u044A", // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 26 */ "\u044B", + /* 27 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* 28 */ "\u044D", // U+0438: "и" CYRILLIC SMALL LETTER I - /* 27 */ "\u0438", + /* 29 */ "\u0438", // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U - /* 28 */ "\u04AF", - /* 29 */ null, + /* 30 */ "\u04AF", // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER - /* 30 */ "\u04A3", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 31 */ "\u044A", + /* 31 */ "\u04A3", /* 32 */ null, + /* 33 */ null, // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O - /* 33 */ "\u04E9", + /* 34 */ "\u04E9", // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 34 */ "\u044A", + /* 35 */ "\u044A", + /* 36~ */ + null, null, null, null, + /* ~39 */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* 40 */ "\u0451", }; /* Language lt: Lithuanian */ @@ -1675,21 +1741,21 @@ public final class KeyboardTextsSet { /* 0~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, - /* ~34 */ + null, null, null, null, null, null, + /* ~35 */ // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE - /* 35 */ "\u0455", + /* 36 */ "\u0455", // U+045C: "ќ" CYRILLIC SMALL LETTER KJE - /* 36 */ "\u045C", + /* 37 */ "\u045C", // U+0437: "з" CYRILLIC SMALL LETTER ZE - /* 37 */ "\u0437", + /* 38 */ "\u0437", // U+0453: "ѓ" CYRILLIC SMALL LETTER GJE - /* 38 */ "\u0453", + /* 39 */ "\u0453", // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE - /* 39 */ "\u0450", + /* 40 */ "\u0450", // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE - /* 40 */ "\u045D", - /* 41 */ null, + /* 41 */ "\u045D", + /* 42 */ null, // U+2018: "‘" LEFT SINGLE QUOTATION MARK // U+2019: "’" RIGHT SINGLE QUOTATION MARK // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK @@ -1700,10 +1766,10 @@ public final class KeyboardTextsSet { // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,„,“,”,‟,«,»</string> - /* 42 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB", + /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB", // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«,»,‘,’,‚,‛</string> - /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", + /* 44 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", }; /* Language nb: Norwegian Bokmål */ @@ -1965,20 +2031,24 @@ public final class KeyboardTextsSet { /* ~24 */ // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA /* 25 */ "\u0449", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* 26 */ "\u044A", // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 26 */ "\u044B", + /* 27 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* 28 */ "\u044D", // U+0438: "и" CYRILLIC SMALL LETTER I - /* 27 */ "\u0438", - /* 28 */ null, - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 29 */ "\u0451", - /* 30 */ null, - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 31 */ "\u044A", - /* 32 */ null, - /* 33 */ null, + /* 29 */ "\u0438", + /* 30~ */ + null, null, null, null, null, + /* ~34 */ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 34 */ "\u044A", + /* 35 */ "\u044A", + /* 36~ */ + null, null, null, null, + /* ~39 */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* 40 */ "\u0451", }; /* Language sk: Slovak */ @@ -2096,21 +2166,40 @@ public final class KeyboardTextsSet { /* 0~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, - /* ~34 */ + null, null, null, null, null, null, + /* ~35 */ + // TODO: Move these to sr-Latn once we can handle IETF language tag with script name specified. + // BEGIN: More keys definitions for Serbian (Latin) + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // <string name="more_keys_for_s">š,ß,ś</string> + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // <string name="more_keys_for_c">č,ç,ć</string> + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // <string name="more_keys_for_d">ď</string> + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // <string name="more_keys_for_z">ž,ź,ż</string> + // END: More keys definitions for Serbian (Latin) + // BEGIN: More keys definitions for Serbian (Cyrillic) // U+0437: "з" CYRILLIC SMALL LETTER ZE - /* 35 */ "\u0437", + /* 36 */ "\u0437", // U+045B: "ћ" CYRILLIC SMALL LETTER TSHE - /* 36 */ "\u045B", + /* 37 */ "\u045B", // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE - /* 37 */ "\u0455", + /* 38 */ "\u0455", // U+0452: "ђ" CYRILLIC SMALL LETTER DJE - /* 38 */ "\u0452", + /* 39 */ "\u0452", // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE - /* 39 */ "\u0450", + /* 40 */ "\u0450", // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE - /* 40 */ "\u045D", - /* 41 */ null, + /* 41 */ "\u045D", + /* 42 */ null, + // END: More keys definitions for Serbian (Cyrillic) // U+2018: "‘" LEFT SINGLE QUOTATION MARK // U+2019: "’" RIGHT SINGLE QUOTATION MARK // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK @@ -2121,10 +2210,10 @@ public final class KeyboardTextsSet { // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,„,“,”,‟,«,»</string> - /* 42 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB", + /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB", // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«,»,‘,’,‚,‛</string> - /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", + /* 44 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", }; /* Language sv: Swedish */ @@ -2169,6 +2258,111 @@ public final class KeyboardTextsSet { /* 24 */ "\u00E6", }; + /* Language sw: Swahili */ + private static final String[] LANGUAGE_sw = { + // This is the same as English except more_keys_for_g. + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113", + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* 5 */ "\u00DF", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* 6 */ "\u00F1", + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + /* 7 */ "\u00E7", + /* 8~ */ + null, null, null, null, null, null, null, + /* ~14 */ + /* 15 */ "g\'", + }; + + /* Language tl: Tagalog */ + private static final String[] LANGUAGE_tl = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* 0 */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* 1 */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* 3 */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + /* 5 */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* 6 */ "\u00F1,\u0144", + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* 7 */ "\u00E7,\u0107,\u010D", + }; + /* Language tr: Turkish */ private static final String[] LANGUAGE_tr = { // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX @@ -2222,20 +2416,23 @@ public final class KeyboardTextsSet { /* ~24 */ // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA /* 25 */ "\u0449", + // U+0457: "ї" CYRILLIC SMALL LETTER YI + /* 26 */ "\u0457", // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - /* 26 */ "\u0456", + /* 27 */ "\u0456", + // U+0454: "є" CYRILLIC SMALL LETTER UKRAINIAN IE + /* 28 */ "\u0454", // U+0438: "и" CYRILLIC SMALL LETTER I - /* 27 */ "\u0438", - /* 28~ */ - null, null, null, - /* ~30 */ - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 31 */ "\u044A", + /* 29 */ "\u0438", + /* 30 */ null, + /* 31 */ null, + // U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN + /* 32 */ "\u0491", // U+0457: "ї" CYRILLIC SMALL LETTER YI - /* 32 */ "\u0457", - /* 33 */ null, + /* 33 */ "\u0457", + /* 34 */ null, // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 34 */ "\u044A", + /* 35 */ "\u044A", }; /* Language vi: Vietnamese */ @@ -2319,6 +2516,53 @@ public final class KeyboardTextsSet { /* 9 */ "\u0111", }; + /* Language zu: Zulu */ + private static final String[] LANGUAGE_zu = { + // This is the same as English + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113", + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* 5 */ "\u00DF", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* 6 */ "\u00F1", + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + /* 7 */ "\u00E7", + }; + /* Language zz: No language */ private static final String[] LANGUAGE_zz = { // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE @@ -2444,6 +2688,7 @@ public final class KeyboardTextsSet { private static final Object[] LANGUAGES_AND_TEXTS = { "DEFAULT", LANGUAGE_DEFAULT, /* default */ + "af", LANGUAGE_af, /* Afrikaans */ "ar", LANGUAGE_ar, /* Arabic */ "be", LANGUAGE_be, /* Belarusian */ "ca", LANGUAGE_ca, /* Catalan */ @@ -2477,9 +2722,12 @@ public final class KeyboardTextsSet { "sl", LANGUAGE_sl, /* Slovenian */ "sr", LANGUAGE_sr, /* Serbian */ "sv", LANGUAGE_sv, /* Swedish */ + "sw", LANGUAGE_sw, /* Swahili */ + "tl", LANGUAGE_tl, /* Tagalog */ "tr", LANGUAGE_tr, /* Turkish */ "uk", LANGUAGE_uk, /* Ukrainian */ "vi", LANGUAGE_vi, /* Vietnamese */ + "zu", LANGUAGE_zu, /* Zulu */ "zz", LANGUAGE_zz, /* No language */ }; diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java new file mode 100644 index 000000000..c38febf49 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2012 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.Paint; +import android.graphics.Paint.Align; +import android.text.TextUtils; +import android.util.SparseArray; +import android.widget.RelativeLayout; + +import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.latin.R; + +public class PreviewPlacerView extends RelativeLayout { + private final Paint mGesturePaint; + private final Paint mTextPaint; + private final int mGestureFloatingPreviewTextColor; + private final int mGestureFloatingPreviewTextOffset; + private final int mGestureFloatingPreviewTextShadowColor; + private final int mGestureFloatingPreviewTextShadowBorder; + private final int mGestureFloatingPreviewTextShadingColor; + private final int mGestureFloatingPreviewTextShadingBorder; + private final int mGestureFloatingPreviewTextConnectorColor; + private final int mGestureFloatingPreviewTextConnectorWidth; + + private int mXOrigin; + private int mYOrigin; + + private final SparseArray<PointerTracker> mPointers = new SparseArray<PointerTracker>(); + + private String mGestureFloatingPreviewText; + private boolean mDrawsGesturePreviewTrail; + private boolean mDrawsGestureFloatingPreviewText; + + public PreviewPlacerView(Context context, TypedArray keyboardViewAttr) { + super(context); + setWillNotDraw(false); + + final int gestureFloatingPreviewTextSize = keyboardViewAttr.getDimensionPixelSize( + R.styleable.KeyboardView_gestureFloatingPreviewTextSize, 0); + mGestureFloatingPreviewTextColor = keyboardViewAttr.getColor( + R.styleable.KeyboardView_gestureFloatingPreviewTextColor, 0); + mGestureFloatingPreviewTextOffset = keyboardViewAttr.getDimensionPixelOffset( + R.styleable.KeyboardView_gestureFloatingPreviewTextOffset, 0); + mGestureFloatingPreviewTextShadowColor = keyboardViewAttr.getColor( + R.styleable.KeyboardView_gestureFloatingPreviewTextShadowColor, 0); + mGestureFloatingPreviewTextShadowBorder = keyboardViewAttr.getDimensionPixelSize( + R.styleable.KeyboardView_gestureFloatingPreviewTextShadowBorder, 0); + mGestureFloatingPreviewTextShadingColor = keyboardViewAttr.getColor( + R.styleable.KeyboardView_gestureFloatingPreviewTextShadingColor, 0); + mGestureFloatingPreviewTextShadingBorder = keyboardViewAttr.getDimensionPixelSize( + R.styleable.KeyboardView_gestureFloatingPreviewTextShadingBorder, 0); + mGestureFloatingPreviewTextConnectorColor = keyboardViewAttr.getColor( + R.styleable.KeyboardView_gestureFloatingPreviewTextConnectorColor, 0); + mGestureFloatingPreviewTextConnectorWidth = keyboardViewAttr.getDimensionPixelSize( + R.styleable.KeyboardView_gestureFloatingPreviewTextConnectorWidth, 0); + final int gesturePreviewTrailColor = keyboardViewAttr.getColor( + R.styleable.KeyboardView_gesturePreviewTrailColor, 0); + final int gesturePreviewTrailWidth = keyboardViewAttr.getDimensionPixelSize( + R.styleable.KeyboardView_gesturePreviewTrailWidth, 0); + + mGesturePaint = new Paint(); + mGesturePaint.setAntiAlias(true); + mGesturePaint.setStyle(Paint.Style.STROKE); + mGesturePaint.setStrokeJoin(Paint.Join.ROUND); + mGesturePaint.setColor(gesturePreviewTrailColor); + mGesturePaint.setStrokeWidth(gesturePreviewTrailWidth); + + mTextPaint = new Paint(); + mTextPaint.setAntiAlias(true); + mTextPaint.setStrokeJoin(Paint.Join.ROUND); + mTextPaint.setTextAlign(Align.CENTER); + mTextPaint.setTextSize(gestureFloatingPreviewTextSize); + } + + public void setOrigin(int x, int y) { + mXOrigin = x; + mYOrigin = y; + } + + public void setGesturePreviewMode(boolean drawsGesturePreviewTrail, + boolean drawsGestureFloatingPreviewText) { + mDrawsGesturePreviewTrail = drawsGesturePreviewTrail; + mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText; + } + + public void invalidatePointer(PointerTracker tracker) { + synchronized (mPointers) { + mPointers.put(tracker.mPointerId, tracker); + // TODO: Should narrow the invalidate region. + invalidate(); + } + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + synchronized (mPointers) { + canvas.translate(mXOrigin, mYOrigin); + final int trackerCount = mPointers.size(); + boolean hasDrawnFloatingPreviewText = false; + for (int index = 0; index < trackerCount; index++) { + final PointerTracker tracker = mPointers.valueAt(index); + if (mDrawsGesturePreviewTrail) { + tracker.drawGestureTrail(canvas, mGesturePaint); + } + // TODO: Figure out more cleaner way to draw gesture preview text. + if (mDrawsGestureFloatingPreviewText && !hasDrawnFloatingPreviewText) { + drawGestureFloatingPreviewText(canvas, tracker, mGestureFloatingPreviewText); + hasDrawnFloatingPreviewText = true; + } + } + canvas.translate(-mXOrigin, -mYOrigin); + } + } + + public void setGestureFloatingPreviewText(String gestureFloatingPreviewText) { + mGestureFloatingPreviewText = gestureFloatingPreviewText; + invalidate(); + } + + private void drawGestureFloatingPreviewText(Canvas canvas, PointerTracker tracker, + String gestureFloatingPreviewText) { + if (TextUtils.isEmpty(gestureFloatingPreviewText)) { + return; + } + + final Paint paint = mTextPaint; + final int lastX = tracker.getLastX(); + final int lastY = tracker.getLastY(); + final int textSize = (int)paint.getTextSize(); + final int canvasWidth = canvas.getWidth(); + + final int halfTextWidth = (int)paint.measureText(gestureFloatingPreviewText) / 2 + textSize; + final int textX = Math.min(Math.max(lastX, halfTextWidth), canvasWidth - halfTextWidth); + + int textY = Math.max(-textSize, lastY - mGestureFloatingPreviewTextOffset); + if (textY < 0) { + // Paint black text shadow if preview extends above keyboard region. + paint.setStyle(Paint.Style.FILL_AND_STROKE); + paint.setColor(mGestureFloatingPreviewTextShadowColor); + paint.setStrokeWidth(mGestureFloatingPreviewTextShadowBorder); + canvas.drawText(gestureFloatingPreviewText, textX, textY, paint); + } + + // Paint the vertical line connecting the touch point to the preview text. + paint.setStyle(Paint.Style.STROKE); + paint.setColor(mGestureFloatingPreviewTextConnectorColor); + paint.setStrokeWidth(mGestureFloatingPreviewTextConnectorWidth); + final int lineTopY = textY - textSize / 4; + canvas.drawLine(lastX, lastY, lastX, lineTopY, paint); + if (lastX != textX) { + // Paint the horizontal line connection the touch point to the preview text. + canvas.drawLine(lastX, lineTopY, textX, lineTopY, paint); + } + + // Paint the shading for the text preview + paint.setStyle(Paint.Style.FILL_AND_STROKE); + paint.setColor(mGestureFloatingPreviewTextShadingColor); + paint.setStrokeWidth(mGestureFloatingPreviewTextShadingBorder); + canvas.drawText(gestureFloatingPreviewText, textX, textY, paint); + + // Paint the text preview + paint.setColor(mGestureFloatingPreviewTextColor); + paint.setStyle(Paint.Style.FILL); + canvas.drawText(gestureFloatingPreviewText, textX, textY, paint); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java b/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java index 2398c0850..9e2cbec52 100644 --- a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java +++ b/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java @@ -14,12 +14,14 @@ * the License. */ -package com.android.inputmethod.keyboard; +package com.android.inputmethod.keyboard.internal; import android.content.Context; import android.util.Log; import android.view.MotionEvent; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.Utils; diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index 5d7995dc2..d101aaf15 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -654,9 +654,12 @@ public class ExpandableDictionary extends Dictionary { --index; mLookedUpString[index] = node.mCode; node = node.mParent; - } while (node != null); + } while (node != null && index > 0); - if (freq >= 0) { + // If node is null, we have a word longer than MAX_WORD_LENGTH in the dictionary. + // It's a little unclear how this can happen, but just in case it does it's safer + // to ignore the word in this case. + if (freq >= 0 && node == null) { suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index, BinaryDictionary.MAX_WORD_LENGTH - index), freq, SuggestedWordInfo.KIND_CORRECTION, mDictType)); diff --git a/java/src/com/android/inputmethod/latin/JniUtils.java b/java/src/com/android/inputmethod/latin/JniUtils.java index 4808b867a..86a3826d8 100644 --- a/java/src/com/android/inputmethod/latin/JniUtils.java +++ b/java/src/com/android/inputmethod/latin/JniUtils.java @@ -31,11 +31,7 @@ public class JniUtils { try { System.loadLibrary(JniLibName.JNI_LIB_NAME); } catch (UnsatisfiedLinkError ule) { - Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME); - if (LatinImeLogger.sDBG) { - throw new RuntimeException( - "Could not load native library " + JniLibName.JNI_LIB_NAME); - } + Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME, ule); } } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index d4b59c4cd..a7446695e 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -60,6 +60,7 @@ import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.compat.CompatUtils; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; +import com.android.inputmethod.compat.InputMethodServiceCompatUtils; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.Keyboard; @@ -82,7 +83,8 @@ import java.util.Locale; * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService implements KeyboardActionListener, - SuggestionStripView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener { + SuggestionStripView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener, + Suggest.SuggestInitializationListener { private static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; private static boolean DEBUG; @@ -174,6 +176,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private AlertDialog mOptionsDialog; + private final boolean mIsHardwareAcceleratedDrawingEnabled; + public final UIHandler mHandler = new UIHandler(this); public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> { @@ -346,6 +350,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen super(); mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); + mIsHardwareAcceleratedDrawingEnabled = + InputMethodServiceCompatUtils.enableHardwareAcceleration(this); + Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); } @Override @@ -426,6 +433,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); } + @Override + public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable) { + mIsMainDictionaryAvailable = isMainDictionaryAvailable; + updateKeyboardViewGestureHandlingModeByMainDictionaryAvailability(); + } + private void initSuggest() { final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); final String localeStr = subtypeLocale.toString(); @@ -437,7 +450,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else { oldContactsDictionary = null; } - mSuggest = new Suggest(this, subtypeLocale); + mSuggest = new Suggest(this /* Context */, subtypeLocale, + this /* SuggestInitializationListener */); if (mCurrentSettings.mCorrectionEnabled) { mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold); } @@ -531,15 +545,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen commitTyped(LastComposedWord.NOT_A_SEPARATOR); mConnection.finishComposingText(); mConnection.endBatchEdit(); - if (isShowingOptionDialog()) + if (isShowingOptionDialog()) { mOptionsDialog.dismiss(); + } } super.onConfigurationChanged(conf); } @Override public View onCreateInputView() { - return mKeyboardSwitcher.onCreateInputView(); + return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); } @Override @@ -651,40 +666,61 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen accessUtils.onStartInputViewInternal(editorInfo, restarting); } - mSubtypeSwitcher.updateParametersOnStartInputView(); + if (!restarting) { + mSubtypeSwitcher.updateParametersOnStartInputView(); + } // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); - mLastSelectionStart = editorInfo.initialSelStart; - mLastSelectionEnd = editorInfo.initialSelEnd; mApplicationSpecifiedCompletions = null; - inputView.closing(); - mEnteredText = null; - resetComposingState(true /* alsoResetLastComposedWord */); - mDeleteCount = 0; - mSpaceState = SPACE_STATE_NONE; - - loadSettings(); + final boolean selectionChanged = mLastSelectionStart != editorInfo.initialSelStart + || mLastSelectionEnd != editorInfo.initialSelEnd; + if (!restarting || selectionChanged) { + // If the selection changed, we reset the input state. Essentially, we come here with + // restarting == true when the app called setText() or similar. We should reset the + // state if the app set the text to something else, but keep it if it set a suggestion + // or something. + mEnteredText = null; + resetComposingState(true /* alsoResetLastComposedWord */); + mDeleteCount = 0; + mSpaceState = SPACE_STATE_NONE; - if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) { - mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold); + if (mSuggestionStripView != null) { + mSuggestionStripView.clear(); + } } - switcher.loadKeyboard(editorInfo, mCurrentSettings); + if (!restarting) { + inputView.closing(); + loadSettings(); + + if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) { + mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold); + } - if (mSuggestionStripView != null) - mSuggestionStripView.clear(); + switcher.loadKeyboard(editorInfo, mCurrentSettings); + updateKeyboardViewGestureHandlingModeByMainDictionaryAvailability(); + } setSuggestionStripShownInternal( isSuggestionsStripVisible(), /* needsInputViewShown */ false); + mLastSelectionStart = editorInfo.initialSelStart; + mLastSelectionEnd = editorInfo.initialSelEnd; + // If we come here something in the text state is very likely to have changed. + // We should update the shift state regardless of whether we are restarting or not, because + // this is not perceived as a layout change that may be disruptive like we may have with + // switcher.loadKeyboard; in apps like Talk, we come here when the text is sent and the + // field gets emptied and we need to re-evaluate the shift state, but not the whole layout + // which would be disruptive. + mKeyboardSwitcher.updateShiftState(); + mHandler.cancelUpdateSuggestionStrip(); mHandler.cancelDoubleSpacesTimer(); inputView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn, mCurrentSettings.mKeyPreviewPopupDismissDelay); - inputView.setProximityCorrectionEnabled(true); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } @@ -856,13 +892,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); - } if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return; mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; if (applicationSpecifiedCompletions == null) { clearSuggestionStrip(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_onDisplayCompletions(null); + } return; } @@ -884,6 +920,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // this case? This says to keep whatever the user typed. mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); setSuggestionStripShown(true); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); + } } private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) { @@ -1003,15 +1042,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final CharSequence typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { mConnection.commitText(typedWord, 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_commitText(typedWord); - } final CharSequence prevWord = addToUserHistoryDictionary(typedWord); mLastComposedWord = mWordComposer.commitWord( LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(), separatorCode, prevWord); } - updateSuggestionStrip(); } // Called from the KeyboardSwitcher which needs to know auto caps state to display @@ -1048,12 +1083,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { mConnection.deleteSurroundingText(2, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(2); - } mConnection.commitText(lastTwo.charAt(1) + " ", 1); if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit(); + ResearchLogger.latinIME_swapSwapperAndSpace(); } mKeyboardSwitcher.updateShiftState(); } @@ -1070,9 +1102,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.cancelDoubleSpacesTimer(); mConnection.deleteSurroundingText(2, 0); mConnection.commitText(". ", 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_doubleSpaceAutoPeriod(); - } mKeyboardSwitcher.updateShiftState(); return true; } @@ -1136,9 +1165,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void performEditorAction(int actionId) { mConnection.performEditorAction(actionId); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_performEditorAction(actionId); - } } private void handleLanguageSwitchKey() { @@ -1175,6 +1201,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. if (code >= '0' && code <= '9') { super.sendKeyChar((char)code); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_sendKeyCodePoint(code); + } return; } @@ -1191,9 +1220,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final String text = new String(new int[] { code }, 0, 1); mConnection.commitText(text, text.length()); } - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_sendKeyCodePoint(code); - } } // Implementation of {@link KeyboardActionListener}. @@ -1205,11 +1231,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mLastKeyTime = when; mConnection.beginBatchEdit(); - - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); - } - final KeyboardSwitcher switcher = mKeyboardSwitcher; // The space state depends only on the last character pressed and its own previous // state. Here, we revert the space state to neutral if the key is actually modifying @@ -1291,21 +1312,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mLastComposedWord.deactivate(); mEnteredText = null; mConnection.endBatchEdit(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); + } } // Called from PointerTracker through the KeyboardActionListener interface @Override - public void onTextInput(CharSequence text) { + public void onTextInput(CharSequence rawText) { mConnection.beginBatchEdit(); commitTyped(LastComposedWord.NOT_A_SEPARATOR); - text = specificTldProcessingOnTextInput(text); + mHandler.postUpdateSuggestionStrip(); + final CharSequence text = specificTldProcessingOnTextInput(rawText); if (SPACE_STATE_PHANTOM == mSpaceState) { sendKeyCodePoint(Keyboard.CODE_SPACE); } mConnection.commitText(text, 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_commitText(text); - } mConnection.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); @@ -1334,6 +1356,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWordComposer.setBatchInputPointers(batchPointers); final SuggestedWords suggestedWords = getSuggestedWords(); showSuggestionStrip(suggestedWords, null); + final String gestureFloatingPreviewText = (suggestedWords.size() > 0) + ? suggestedWords.getWord(0) : null; + mKeyboardSwitcher.getKeyboardView() + .showGestureFloatingPreviewText(gestureFloatingPreviewText); } @Override @@ -1341,6 +1367,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWordComposer.setBatchInputPointers(batchPointers); final SuggestedWords suggestedWords = getSuggestedWords(); showSuggestionStrip(suggestedWords, null); + mKeyboardSwitcher.getKeyboardView().showGestureFloatingPreviewText(null); if (suggestedWords == null || suggestedWords.size() == 0) { return; } @@ -1395,9 +1422,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // like the smiley key or the .com key. final int length = mEnteredText.length(); mConnection.deleteSurroundingText(length, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(length); - } // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. // In addition we know that spaceState is false, and that we should not be // reverting any autocorrect at this point. So we can safely return. @@ -1417,9 +1441,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.postUpdateSuggestionStrip(); } else { mConnection.deleteSurroundingText(1, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(1); - } } } else { if (mLastComposedWord.canRevertCommit()) { @@ -1448,9 +1469,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart; mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); mConnection.deleteSurroundingText(lengthToDelete, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete); - } } else { // There is no selection, just delete one character. if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { @@ -1469,14 +1487,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else { mConnection.deleteSurroundingText(1, 0); } - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(1); - } if (mDeleteCount > DELETE_ACCELERATE_AT) { mConnection.deleteSurroundingText(1, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(1); - } } } if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) { @@ -1579,11 +1591,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen boolean didAutoCorrect = false; // Handle separator if (mWordComposer.isComposingWord()) { - // 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 (mCurrentSettings.mCorrectionEnabled && primaryCode != Keyboard.CODE_SINGLE_QUOTE) { + if (mCurrentSettings.mCorrectionEnabled) { commitCurrentAutoCorrection(primaryCode); didAutoCorrect = true; } else { @@ -1617,13 +1625,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (swapWeakSpace) { swapSwapperAndSpace(); mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; - } else if (SPACE_STATE_PHANTOM == spaceState) { + } else if (SPACE_STATE_PHANTOM == spaceState + && !mCurrentSettings.isWeakSpaceStripper(primaryCode)) { // If we are in phantom space state, and the user presses a separator, we want to // stay in phantom space state so that the next keypress has a chance to add the // space. For example, if I type "Good dat", pick "day" from the suggestion strip // then insert a comma and go on to typing the next word, I want the space to be // inserted automatically before the next word, the same way it is when I don't // input the comma. + // The case is a little different if the separator is a space stripper. Such a + // separator does not normally need a space on the right (that's the difference + // between swappers and strippers), so we should not stay in phantom space state if + // the separator is a stripper. Hence the additional test above. mSpaceState = SPACE_STATE_PHANTOM; } @@ -1647,8 +1660,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen commitTyped(LastComposedWord.NOT_A_SEPARATOR); requestHideSelf(0); MainKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); - if (inputView != null) + if (inputView != null) { inputView.closing(); + } } // TODO: make this private @@ -1799,10 +1813,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + "is empty? Impossible! I must commit suicide."); } Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord, - autoCorrection.toString()); - } mExpectingUpdateSelection = true; commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separatorCodePoint); @@ -1828,13 +1838,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // So, LatinImeLogger logs "" as a user's input. LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords); // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y); - } final int primaryCode = suggestion.charAt(0); onCodeInput(primaryCode, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y); + } return; } @@ -1861,10 +1871,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; mConnection.commitCompletion(completionInfo); mConnection.endBatchEdit(); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index, - completionInfo.getText(), x, y); - } return; } @@ -1873,12 +1879,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final String replacedWord = mWordComposer.getTypedWord().toString(); LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion.toString(), index, suggestedWords); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y); - } mExpectingUpdateSelection = true; commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y); + } mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); @@ -1913,9 +1919,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions(); mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_commitText(chosenWord); - } // Add the word to the user history dictionary final CharSequence prevWord = addToUserHistoryDictionary(chosenWord); // TODO: figure out here if this is an auto-correct or if the best word is actually @@ -1982,9 +1985,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); final int length = word.length(); mConnection.deleteSurroundingText(length, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(length); - } mConnection.setComposingText(word, 1); mHandler.postUpdateSuggestionStrip(); } @@ -2012,9 +2012,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } mConnection.deleteSurroundingText(deleteLength, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(deleteLength); - } if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { mUserHistoryDictionary.cancelAddingUserHistory( previousWord.toString(), committedWord.toString()); @@ -2044,18 +2041,30 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void onRefreshKeyboard() { // When the device locale is changed in SetupWizard etc., this method may get called via // onConfigurationChanged before SoftInputWindow is shown. + initSuggest(); + loadSettings(); if (mKeyboardSwitcher.getKeyboardView() != null) { // Reload keyboard because the current language has been changed. mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings); + updateKeyboardViewGestureHandlingModeByMainDictionaryAvailability(); } - initSuggest(); - loadSettings(); // Since we just changed languages, we should re-evaluate suggestions with whatever word // we are currently composing. If we are not composing anything, we may want to display // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway). mHandler.postUpdateSuggestionStrip(); } + private void updateKeyboardViewGestureHandlingModeByMainDictionaryAvailability() { + final MainKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView(); + if (keyboardView != null) { + final boolean shouldHandleGesture = mCurrentSettings.mGestureInputEnabled + && mIsMainDictionaryAvailable; + keyboardView.setGestureHandlingMode(shouldHandleGesture, + mCurrentSettings.mGesturePreviewTrailEnabled, + mCurrentSettings.mGestureFloatingPreviewTextEnabled); + } + } + // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to // {@link KeyboardSwitcher}. Called from KeyboardSwitcher public void hapticAndAudioFeedback(final int primaryCode) { diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 8b4c17322..41e59e92d 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -55,7 +55,9 @@ public class RichInputConnection { public void beginBatchEdit() { if (++mNestLevel == 1) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) mIC.beginBatchEdit(); + if (null != mIC) { + mIC.beginBatchEdit(); + } } else { if (DBG) { throw new RuntimeException("Nest level too deep"); @@ -66,7 +68,9 @@ public class RichInputConnection { } public void endBatchEdit() { if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead - if (--mNestLevel == 0 && null != mIC) mIC.endBatchEdit(); + if (--mNestLevel == 0 && null != mIC) { + mIC.endBatchEdit(); + } } private void checkBatchEdit() { @@ -79,12 +83,22 @@ public class RichInputConnection { public void finishComposingText() { checkBatchEdit(); - if (null != mIC) mIC.finishComposingText(); + if (null != mIC) { + mIC.finishComposingText(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_finishComposingText(); + } + } } public void commitText(final CharSequence text, final int i) { checkBatchEdit(); - if (null != mIC) mIC.commitText(text, i); + if (null != mIC) { + mIC.commitText(text, i); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitText(text, i); + } + } } public int getCursorCapsMode(final int inputType) { @@ -107,37 +121,72 @@ public class RichInputConnection { public void deleteSurroundingText(final int i, final int j) { checkBatchEdit(); - if (null != mIC) mIC.deleteSurroundingText(i, j); + if (null != mIC) { + mIC.deleteSurroundingText(i, j); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_deleteSurroundingText(i, j); + } + } } public void performEditorAction(final int actionId) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) mIC.performEditorAction(actionId); + if (null != mIC) { + mIC.performEditorAction(actionId); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_performEditorAction(actionId); + } + } } public void sendKeyEvent(final KeyEvent keyEvent) { checkBatchEdit(); - if (null != mIC) mIC.sendKeyEvent(keyEvent); + if (null != mIC) { + mIC.sendKeyEvent(keyEvent); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_sendKeyEvent(keyEvent); + } + } } public void setComposingText(final CharSequence text, final int i) { checkBatchEdit(); - if (null != mIC) mIC.setComposingText(text, i); + if (null != mIC) { + mIC.setComposingText(text, i); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_setComposingText(text, i); + } + } } public void setSelection(final int from, final int to) { checkBatchEdit(); - if (null != mIC) mIC.setSelection(from, to); + if (null != mIC) { + mIC.setSelection(from, to); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_setSelection(from, to); + } + } } public void commitCorrection(final CorrectionInfo correctionInfo) { checkBatchEdit(); - if (null != mIC) mIC.commitCorrection(correctionInfo); + if (null != mIC) { + mIC.commitCorrection(correctionInfo); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitCorrection(correctionInfo); + } + } } public void commitCompletion(final CompletionInfo completionInfo) { checkBatchEdit(); - if (null != mIC) mIC.commitCompletion(completionInfo); + if (null != mIC) { + mIC.commitCompletion(completionInfo); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitCompletion(completionInfo); + } + } } public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) { @@ -315,9 +364,6 @@ public class RichInputConnection { if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { deleteSurroundingText(1, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(1); - } } } @@ -382,13 +428,7 @@ public class RichInputConnection { return false; } deleteSurroundingText(2, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(2); - } commitText(" ", 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit(); - } return true; } @@ -409,13 +449,7 @@ public class RichInputConnection { return false; } deleteSurroundingText(2, 0); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_deleteSurroundingText(2); - } commitText(" " + textBeforeCursor.subSequence(0, 1), 1); - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_revertSwapPunctuation(); - } return true; } } diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index 45608f439..6251c9acd 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -62,6 +62,7 @@ public class Settings extends InputMethodSettingsFragment public static final String PREF_LAST_USER_DICTIONARY_WRITE_TIME = "last_user_dictionary_write_time"; public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings"; + public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict"; public static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY = "pref_suppress_language_switch_key"; public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST = @@ -69,13 +70,15 @@ public class Settings extends InputMethodSettingsFragment public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles"; 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_BIGRAM_PREDICTIONS = "next_word_prediction"; public static final String PREF_GESTURE_INPUT = "gesture_input"; public static final String PREF_VIBRATION_DURATION_SETTINGS = "pref_vibration_duration_settings"; public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume"; + public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail"; + public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT = + "pref_gesture_floating_preview_text"; public static final String PREF_INPUT_LANGUAGE = "input_language"; public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; @@ -94,13 +97,17 @@ public class Settings extends InputMethodSettingsFragment private TextView mKeypressVibrationDurationSettingsTextView; private TextView mKeypressSoundVolumeSettingsTextView; + private static void setPreferenceEnabled(Preference preference, boolean enabled) { + if (preference != null) { + preference.setEnabled(enabled); + } + } + private void ensureConsistencyOfAutoCorrectionSettings() { final String autoCorrectionOff = getResources().getString( R.string.auto_correction_threshold_mode_index_off); final String currentSetting = mAutoCorrectionThresholdPreference.getValue(); - if (null != mBigramPrediction) { - mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff)); - } + setPreferenceEnabled(mBigramPrediction, !currentSetting.equals(autoCorrectionOff)); } @Override @@ -180,13 +187,11 @@ public class Settings extends InputMethodSettingsFragment if (null == mKeyPreviewPopupDismissDelay.getValue()) { mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue); } - mKeyPreviewPopupDismissDelay.setEnabled( + setPreferenceEnabled(mKeyPreviewPopupDismissDelay, SettingsValues.isKeyPreviewPopupEnabled(prefs, res)); } - final CheckBoxPreference includeOtherImesInLanguageSwitchList = - (CheckBoxPreference)findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST); - includeOtherImesInLanguageSwitchList.setEnabled( + setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST), !SettingsValues.isLanguageSwitchKeySupressed(prefs)); final PreferenceScreen dictionaryLink = @@ -200,10 +205,19 @@ public class Settings extends InputMethodSettingsFragment final boolean gestureInputEnabledByBuildConfig = res.getBoolean( R.bool.config_gesture_input_enabled_by_build_config); + final Preference gesturePreviewTrail = findPreference(PREF_GESTURE_PREVIEW_TRAIL); + final Preference gestureFloatingPreviewText = findPreference( + PREF_GESTURE_FLOATING_PREVIEW_TEXT); if (!gestureInputEnabledByBuildConfig) { - final Preference gestureInputPref = findPreference(PREF_GESTURE_INPUT); - miscSettings.removePreference(gestureInputPref); + miscSettings.removePreference(findPreference(PREF_GESTURE_INPUT)); + miscSettings.removePreference(gesturePreviewTrail); + miscSettings.removePreference(gestureFloatingPreviewText); + } else { + final boolean gestureInputEnabledByUser = prefs.getBoolean(PREF_GESTURE_INPUT, true); + setPreferenceEnabled(gesturePreviewTrail, gestureInputEnabledByUser); + setPreferenceEnabled(gestureFloatingPreviewText, gestureInputEnabledByUser); } + final boolean showUsabilityStudyModeOption = res.getBoolean(R.bool.config_enable_usability_study_mode_option) || ProductionFlag.IS_EXPERIMENTAL || ENABLE_EXPERIMENTAL_SETTINGS; @@ -277,17 +291,22 @@ public class Settings extends InputMethodSettingsFragment public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { (new BackupManager(getActivity())).dataChanged(); if (key.equals(PREF_POPUP_ON)) { - final ListPreference popupDismissDelay = - (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); - if (null != popupDismissDelay) { - popupDismissDelay.setEnabled(prefs.getBoolean(PREF_POPUP_ON, true)); - } + setPreferenceEnabled(findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY), + prefs.getBoolean(PREF_POPUP_ON, true)); } else if (key.equals(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) { - final CheckBoxPreference includeOtherImesInLanguageSwicthList = - (CheckBoxPreference)findPreference( - PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST); - includeOtherImesInLanguageSwicthList.setEnabled( + setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST), !SettingsValues.isLanguageSwitchKeySupressed(prefs)); + } else if (key.equals(PREF_GESTURE_INPUT)) { + final boolean gestureInputEnabledByConfig = getResources().getBoolean( + R.bool.config_gesture_input_enabled_by_build_config); + if (gestureInputEnabledByConfig) { + final boolean gestureInputEnabledByUser = prefs.getBoolean( + PREF_GESTURE_INPUT, true); + setPreferenceEnabled(findPreference(PREF_GESTURE_PREVIEW_TRAIL), + gestureInputEnabledByUser); + setPreferenceEnabled(findPreference(PREF_GESTURE_FLOATING_PREVIEW_TEXT), + gestureInputEnabledByUser); + } } ensureConsistencyOfAutoCorrectionSettings(); updateVoiceModeSummary(); @@ -335,16 +354,18 @@ public class Settings extends InputMethodSettingsFragment private void refreshEnablingsOfKeypressSoundAndVibrationSettings( SharedPreferences sp, Resources res) { if (mKeypressVibrationDurationSettingsPref != null) { - final boolean hasVibrator = VibratorUtils.getInstance(getActivity()).hasVibrator(); - final boolean vibrateOn = hasVibrator && sp.getBoolean(Settings.PREF_VIBRATE_ON, + final boolean hasVibratorHardware = VibratorUtils.getInstance(getActivity()) + .hasVibrator(); + final boolean vibrateOnByUser = sp.getBoolean(Settings.PREF_VIBRATE_ON, res.getBoolean(R.bool.config_default_vibration_enabled)); - mKeypressVibrationDurationSettingsPref.setEnabled(vibrateOn); + setPreferenceEnabled(mKeypressVibrationDurationSettingsPref, + hasVibratorHardware && vibrateOnByUser); } if (mKeypressSoundVolumeSettingsPref != null) { final boolean soundOn = sp.getBoolean(Settings.PREF_SOUND_ON, res.getBoolean(R.bool.config_default_sound_enabled)); - mKeypressSoundVolumeSettingsPref.setEnabled(soundOn); + setPreferenceEnabled(mKeypressSoundVolumeSettingsPref, soundOn); } } diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java index 3ed981375..0843bdbbc 100644 --- a/java/src/com/android/inputmethod/latin/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/SettingsValues.java @@ -84,6 +84,8 @@ public class SettingsValues { private final float mKeypressSoundVolumeRawValue; private final InputMethodSubtype[] mAdditionalSubtypes; public final boolean mGestureInputEnabled; + public final boolean mGesturePreviewTrailEnabled; + public final boolean mGestureFloatingPreviewTextEnabled; // From the input box private final InputAttributes mInputAttributes; @@ -174,6 +176,9 @@ public class SettingsValues { R.bool.config_gesture_input_enabled_by_build_config); mGestureInputEnabled = gestureInputEnabledByBuildConfig && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true); + mGesturePreviewTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true); + mGestureFloatingPreviewTextEnabled = prefs.getBoolean( + Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; mSuggestionVisibility = createSuggestionVisibility(res); } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 598ef1de7..5e2a04124 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -42,6 +42,10 @@ public class Suggest { // TODO: rename this to CORRECTION_ON public static final int CORRECTION_FULL = 1; + public interface SuggestInitializationListener { + public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); + } + private static final boolean DBG = LatinImeLogger.sDBG; private Dictionary mMainDictionary; @@ -55,11 +59,14 @@ public class Suggest { private float mAutoCorrectionThreshold; // Locale used for upper- and title-casing words - final private Locale mLocale; + private final Locale mLocale; + private final SuggestInitializationListener mListener; - public Suggest(final Context context, final Locale locale) { + public Suggest(final Context context, final Locale locale, + final SuggestInitializationListener listener) { initAsynchronously(context, locale); mLocale = locale; + mListener = listener; } /* package for test */ Suggest(final Context context, final File dictionary, @@ -67,6 +74,7 @@ public class Suggest { final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary, startOffset, length /* useFullEditDistance */, false, locale); mLocale = locale; + mListener = null; mMainDictionary = mainDict; addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, mainDict); initWhitelistAndAutocorrectAndPool(context, locale); @@ -98,6 +106,9 @@ public class Suggest { public void resetMainDict(final Context context, final Locale locale) { mMainDictionary = null; + if (mListener != null) { + mListener.onUpdateMainDictionaryAvailability(hasMainDictionary()); + } new Thread("InitializeBinaryDictionary") { @Override public void run() { @@ -105,6 +116,9 @@ public class Suggest { DictionaryFactory.createMainDictionaryFromManager(context, locale); addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, newMainDict); mMainDictionary = newMainDict; + if (mListener != null) { + mListener.onUpdateMainDictionaryAvailability(hasMainDictionary()); + } } }.start(); } @@ -219,7 +233,7 @@ public class Suggest { // the current settings. It may also be useful to know, when the setting is off, whether // the word *would* have been auto-corrected. if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord() - || suggestionsSet.isEmpty() + || suggestionsSet.isEmpty() || wordComposer.hasDigits() || wordComposer.isMostlyCaps() || wordComposer.isResumed() || !hasMainDictionary()) { // If we don't have a main dictionary, we never want to auto-correct. The reason for diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 6d346d179..5606a58e4 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -41,6 +41,7 @@ public class WordComposer { // Cache these values for performance private int mCapsCount; + private int mDigitsCount; private boolean mAutoCapitalized; private int mTrailingSingleQuotesCount; private int mCodePointSize; @@ -65,6 +66,7 @@ public class WordComposer { mTypedWord = new StringBuilder(source.mTypedWord); mInputPointers.copy(source.mInputPointers); mCapsCount = source.mCapsCount; + mDigitsCount = source.mDigitsCount; mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; mAutoCapitalized = source.mAutoCapitalized; mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; @@ -80,6 +82,7 @@ public class WordComposer { mTypedWord.setLength(0); mAutoCorrection = null; mCapsCount = 0; + mDigitsCount = 0; mIsFirstCharCapitalized = false; mTrailingSingleQuotesCount = 0; mIsResumed = false; @@ -141,6 +144,7 @@ public class WordComposer { mIsFirstCharCapitalized = isFirstCharCapitalized( newIndex, primaryCode, mIsFirstCharCapitalized); if (Character.isUpperCase(primaryCode)) mCapsCount++; + if (Character.isDigit(primaryCode)) mDigitsCount++; if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) { ++mTrailingSingleQuotesCount; } else { @@ -213,6 +217,7 @@ public class WordComposer { mTypedWord.deleteCharAt(stringBuilderLength - 1); } if (Character.isUpperCase(lastChar)) mCapsCount--; + if (Character.isDigit(lastChar)) mDigitsCount--; refreshSize(); } // We may have deleted the last one. @@ -268,6 +273,13 @@ public class WordComposer { } /** + * Returns true if we have digits in the composing word. + */ + public boolean hasDigits() { + return mDigitsCount > 0; + } + + /** * Saves the reason why the word is capitalized - whether it was automatic or * due to the user hitting shift in the middle of a sentence. * @param auto whether it was an automatic capitalization due to start of sentence @@ -322,6 +334,7 @@ public class WordComposer { lastComposedWord.deactivate(); } mCapsCount = 0; + mDigitsCount = 0; mIsBatchMode = false; mTypedWord.setLength(0); mTrailingSingleQuotesCount = 0; diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index 8b53c9427..5864db28e 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -61,8 +61,8 @@ public class FusionDictionary implements Iterable<Word> { * This represents an "attribute", that is either a bigram or a shortcut. */ public static class WeightedString { - final String mWord; - int mFrequency; + public final String mWord; + public int mFrequency; public WeightedString(String word, int frequency) { mWord = word; mFrequency = frequency; diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java index d07826757..65fc72c40 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Word.java +++ b/java/src/com/android/inputmethod/latin/makedict/Word.java @@ -27,10 +27,10 @@ import java.util.Arrays; * This is chiefly used to iterate a dictionary. */ public class Word implements Comparable<Word> { - final String mWord; - final int mFrequency; - final ArrayList<WeightedString> mShortcutTargets; - final ArrayList<WeightedString> mBigrams; + public final String mWord; + public final int mFrequency; + public final ArrayList<WeightedString> mShortcutTargets; + public final ArrayList<WeightedString> mBigrams; private int mHashCode = 0; diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java index c9f3b476a..11eae8813 100644 --- a/java/src/com/android/inputmethod/research/FeedbackActivity.java +++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java @@ -18,10 +18,7 @@ package com.android.inputmethod.research; import android.app.Activity; import android.os.Bundle; -import android.text.Editable; -import android.view.View; import android.widget.CheckBox; -import android.widget.EditText; import com.android.inputmethod.latin.R; @@ -31,6 +28,11 @@ public class FeedbackActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.research_feedback_activity); final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout); + final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history); + final CharSequence cs = checkbox.getText(); + final String actualString = String.format(cs.toString(), + ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE); + checkbox.setText(actualString); layout.setActivity(this); } diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java new file mode 100644 index 000000000..65f5f83ae --- /dev/null +++ b/java/src/com/android/inputmethod/research/LogBuffer.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2012 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.research; + +import java.util.LinkedList; + +/** + * A buffer that holds a fixed number of LogUnits. + * + * LogUnits are added in and shifted out in temporal order. Only a subset of the LogUnits are + * actual words; the other LogUnits do not count toward the word limit. Once the buffer reaches + * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to + * stay under the capacity limit. + */ +public class LogBuffer { + protected final LinkedList<LogUnit> mLogUnits; + /* package for test */ int mWordCapacity; + // The number of members of mLogUnits that are actual words. + protected int mNumActualWords; + + /** + * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and + * unlimited number of non-word LogUnits), and that outputs its result to a researchLog. + * + * @param wordCapacity maximum number of words + */ + LogBuffer(final int wordCapacity) { + if (wordCapacity <= 0) { + throw new IllegalArgumentException("wordCapacity must be 1 or greater."); + } + mLogUnits = new LinkedList<LogUnit>(); + mWordCapacity = wordCapacity; + mNumActualWords = 0; + } + + /** + * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's + * (oldest first) if word capacity is reached. + */ + public void shiftIn(LogUnit newLogUnit) { + if (newLogUnit.getWord() == null) { + // This LogUnit isn't a word, so it doesn't count toward the word-limit. + mLogUnits.add(newLogUnit); + return; + } + if (mNumActualWords == mWordCapacity) { + shiftOutThroughFirstWord(); + } + mLogUnits.add(newLogUnit); + mNumActualWords++; // Must be a word, or we wouldn't be here. + } + + private void shiftOutThroughFirstWord() { + while (!mLogUnits.isEmpty()) { + final LogUnit logUnit = mLogUnits.removeFirst(); + onShiftOut(logUnit); + if (logUnit.hasWord()) { + // Successfully shifted out a word-containing LogUnit and made space for the new + // LogUnit. + mNumActualWords--; + break; + } + } + } + + /** + * Removes all LogUnits from the buffer without calling onShiftOut(). + */ + public void clear() { + mLogUnits.clear(); + mNumActualWords = 0; + } + + /** + * Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn. LogUnits are + * removed in the order entered. This method is not called when shiftOut is called directly. + * + * Base class does nothing; subclasses may override. + */ + protected void onShiftOut(LogUnit logUnit) { + } + + /** + * Called to deliberately remove the oldest LogUnit. Usually called when draining the + * LogBuffer. + */ + public LogUnit shiftOut() { + if (mLogUnits.isEmpty()) { + return null; + } + final LogUnit logUnit = mLogUnits.removeFirst(); + if (logUnit.hasWord()) { + mNumActualWords--; + } + return logUnit; + } +} diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java new file mode 100644 index 000000000..8a80664f5 --- /dev/null +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2012 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.research; + +import java.util.ArrayList; + +/** + * A group of log statements related to each other. + * + * A LogUnit is collection of LogStatements, each of which is generated by at a particular point + * in the code. (There is no LogStatement class; the data is stored across the instance variables + * here.) A single LogUnit's statements can correspond to all the calls made while in the same + * composing region, or all the calls between committing the last composing region, and the first + * character of the next composing region. + * + * Individual statements in a log may be marked as potentially private. If so, then they are only + * published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit + * will not violate the user's privacy. Checks for this may include whether other LogUnits have + * been published recently, or whether the LogUnit contains numbers, etc. + */ +/* package */ class LogUnit { + private final ArrayList<String[]> mKeysList = new ArrayList<String[]>(); + private final ArrayList<Object[]> mValuesList = new ArrayList<Object[]>(); + private final ArrayList<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>(); + private String mWord; + private boolean mContainsDigit; + + public void addLogStatement(final String[] keys, final Object[] values, + final Boolean isPotentiallyPrivate) { + mKeysList.add(keys); + mValuesList.add(values); + mIsPotentiallyPrivate.add(isPotentiallyPrivate); + } + + public void publishTo(final ResearchLog researchLog, final boolean isIncludingPrivateData) { + final int size = mKeysList.size(); + for (int i = 0; i < size; i++) { + if (!mIsPotentiallyPrivate.get(i) || isIncludingPrivateData) { + researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); + } + } + } + + public void setWord(String word) { + mWord = word; + } + + public String getWord() { + return mWord; + } + + public boolean hasWord() { + return mWord != null; + } + + public void setContainsDigit() { + mContainsDigit = true; + } + + public boolean hasDigit() { + return mContainsDigit; + } + + public boolean isEmpty() { + return mKeysList.isEmpty(); + } +} diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java new file mode 100644 index 000000000..745768d35 --- /dev/null +++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2012 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.research; + +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.Suggest; + +import java.util.Random; + +public class MainLogBuffer extends LogBuffer { + // The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams. + private static final int N_GRAM_SIZE = 2; + // The number of words between n-grams to omit from the log. + private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = 18; + + private final ResearchLog mResearchLog; + private Suggest mSuggest; + + // The minimum periodicity with which n-grams can be sampled. E.g. mWinWordPeriod is 10 if + // every 10th bigram is sampled, i.e., words 1-8 are not, but the bigram at words 9 and 10, etc. + // for 11-18, and the bigram at words 19 and 20. If an n-gram is not safe (e.g. it contains a + // number in the middle or an out-of-vocabulary word), then sampling is delayed until a safe + // n-gram does appear. + /* package for test */ int mMinWordPeriod; + + // Counter for words left to suppress before an n-gram can be sampled. Reset to mMinWordPeriod + // after a sample is taken. + /* package for test */ int mWordsUntilSafeToSample; + + public MainLogBuffer(final ResearchLog researchLog) { + super(N_GRAM_SIZE); + mResearchLog = researchLog; + mMinWordPeriod = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES + N_GRAM_SIZE; + final Random random = new Random(); + mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod); + } + + public void setSuggest(Suggest suggest) { + mSuggest = suggest; + } + + @Override + public void shiftIn(final LogUnit newLogUnit) { + super.shiftIn(newLogUnit); + if (newLogUnit.hasWord()) { + if (mWordsUntilSafeToSample > 0) { + mWordsUntilSafeToSample--; + } + } + } + + public void resetWordCounter() { + mWordsUntilSafeToSample = mMinWordPeriod; + } + + /** + * Determines whether the content of the MainLogBuffer can be safely uploaded in its complete + * form and still protect the user's privacy. + * + * The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any + * non-character data that is typed between words. The decision about privacy is made based on + * the buffer's entire content. If it is decided that the privacy risks are too great to upload + * the contents of this buffer, a censored version of the LogItems may still be uploaded. E.g., + * the screen orientation and other characteristics about the device can be uploaded without + * revealing much about the user. + */ + public boolean isSafeToLog() { + // Check that we are not sampling too frequently. Having sampled recently might disclose + // too much of the user's intended meaning. + if (mWordsUntilSafeToSample > 0) { + return false; + } + if (mSuggest == null || !mSuggest.hasMainDictionary()) { + // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a word + // is out-of-vocabulary or not. Therefore, we must judge the entire buffer contents to + // potentially pose a privacy risk. + return false; + } + // Reload the dictionary in case it has changed (e.g., because the user has changed + // languages). + final Dictionary dictionary = mSuggest.getMainDictionary(); + if (dictionary == null) { + return false; + } + // Check each word in the buffer. If any word poses a privacy threat, we cannot upload the + // complete buffer contents in detail. + final int length = mLogUnits.size(); + for (int i = 0; i < length; i++) { + final LogUnit logUnit = mLogUnits.get(i); + final String word = logUnit.getWord(); + if (word == null) { + // Digits outside words are a privacy threat. + if (logUnit.hasDigit()) { + return false; + } + } else { + // Words not in the dictionary are a privacy threat. + if (!(dictionary.isValidWord(word))) { + return false; + } + } + } + // All checks have passed; this buffer's content can be safely uploaded. + return true; + } + + @Override + protected void onShiftOut(LogUnit logUnit) { + if (mResearchLog != null) { + mResearchLog.publish(logUnit, false /* isIncludingPrivateData */); + } + } +} diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index 18bf3c07f..71a6d6a78 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -26,7 +26,6 @@ import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.research.ResearchLogger.LogUnit; import java.io.BufferedWriter; import java.io.File; @@ -37,6 +36,7 @@ import java.io.OutputStreamWriter; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -51,21 +51,22 @@ import java.util.concurrent.TimeUnit; */ public class ResearchLog { private static final String TAG = ResearchLog.class.getSimpleName(); - private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( - new OutputStreamWriter(new NullOutputStream())); + private static final boolean DEBUG = false; + private static final long FLUSH_DELAY_IN_MS = 1000 * 5; + private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4; - final ScheduledExecutorService mExecutor; + /* package */ final ScheduledExecutorService mExecutor; /* package */ final File mFile; private JsonWriter mJsonWriter = NULL_JSON_WRITER; + // true if at least one byte of data has been written out to the log file. This must be + // remembered because JsonWriter requires that calls matching calls to beginObject and + // endObject, as well as beginArray and endArray, and the file is opened lazily, only when + // it is certain that data will be written. Alternatively, the matching call exceptions + // could be caught, but this might suppress other errors. + private boolean mHasWrittenData = false; - private int mLoggingState; - private static final int LOGGING_STATE_UNSTARTED = 0; - private static final int LOGGING_STATE_READY = 1; // don't create file until necessary - private static final int LOGGING_STATE_RUNNING = 2; - private static final int LOGGING_STATE_STOPPING = 3; - private static final int LOGGING_STATE_STOPPED = 4; - private static final long FLUSH_DELAY_IN_MS = 1000 * 5; - + private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( + new OutputStreamWriter(new NullOutputStream())); private static class NullOutputStream extends OutputStream { /** {@inheritDoc} */ @Override @@ -84,128 +85,81 @@ public class ResearchLog { } } - public ResearchLog(File outputFile) { - mExecutor = Executors.newSingleThreadScheduledExecutor(); + public ResearchLog(final File outputFile) { if (outputFile == null) { throw new IllegalArgumentException(); } + mExecutor = Executors.newSingleThreadScheduledExecutor(); mFile = outputFile; - mLoggingState = LOGGING_STATE_UNSTARTED; - } - - public synchronized void start() throws IOException { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_READY; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - break; - } } - public synchronized void stop() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_STOPPED; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - try { - mJsonWriter.endArray(); - mJsonWriter.flush(); - mJsonWriter.close(); - } finally { - boolean success = mFile.setWritable(false, false); - mLoggingState = LOGGING_STATE_STOPPED; - } - return null; + public synchronized void close() { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + try { + if (mHasWrittenData) { + mJsonWriter.endArray(); + mJsonWriter.flush(); + mJsonWriter.close(); + mHasWrittenData = false; } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - mLoggingState = LOGGING_STATE_STOPPING; - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + } catch (Exception e) { + Log.d(TAG, "error when closing ResearchLog:"); + e.printStackTrace(); + } finally { + if (mFile.exists()) { + mFile.setWritable(false, false); + } + } + return null; + } + }); + removeAnyScheduledFlush(); + mExecutor.shutdown(); } - public boolean isAlive() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - return true; - } - return false; - } + private boolean mIsAbortSuccessful; - public void waitUntilStopped(final int timeoutInMs) throws InterruptedException { + public synchronized void abort() { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + try { + if (mHasWrittenData) { + mJsonWriter.endArray(); + mJsonWriter.close(); + mHasWrittenData = false; + } + } finally { + mIsAbortSuccessful = mFile.delete(); + } + return null; + } + }); removeAnyScheduledFlush(); mExecutor.shutdown(); - mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS); } - public synchronized void abort() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_STOPPED; - isAbortSuccessful = true; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - try { - mJsonWriter.endArray(); - mJsonWriter.close(); - } finally { - isAbortSuccessful = mFile.delete(); - } - return null; - } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - mLoggingState = LOGGING_STATE_STOPPING; - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + public boolean blockingAbort() throws InterruptedException { + abort(); + mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); + return mIsAbortSuccessful; } - private boolean isAbortSuccessful; - public boolean isAbortSuccessful() { - return isAbortSuccessful; + public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException { + mExecutor.awaitTermination(delay, timeUnit); } /* package */ synchronized void flush() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - removeAnyScheduledFlush(); - mExecutor.submit(mFlushCallable); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + removeAnyScheduledFlush(); + mExecutor.submit(mFlushCallable); } - private Callable<Object> mFlushCallable = new Callable<Object>() { + private final Callable<Object> mFlushCallable = new Callable<Object>() { @Override public Object call() throws Exception { - if (mLoggingState == LOGGING_STATE_RUNNING) { - mJsonWriter.flush(); - } + mJsonWriter.flush(); return null; } }; @@ -224,56 +178,40 @@ public class ResearchLog { mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); } - public synchronized void publishPublicEvents(final LogUnit logUnit) { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - logUnit.publishPublicEventsTo(ResearchLog.this); - scheduleFlush(); - return null; - } - }); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } - } - - public synchronized void publishAllEvents(final LogUnit logUnit) { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - logUnit.publishAllEventsTo(ResearchLog.this); - scheduleFlush(); - return null; - } - }); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: + public synchronized void publish(final LogUnit logUnit, final boolean isIncludingPrivateData) { + try { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + logUnit.publishTo(ResearchLog.this, isIncludingPrivateData); + scheduleFlush(); + return null; + } + }); + } catch (RejectedExecutionException e) { + // TODO: Add code to record loss of data, and report. } } private static final String CURRENT_TIME_KEY = "_ct"; private static final String UPTIME_KEY = "_ut"; private static final String EVENT_TYPE_KEY = "_ty"; + void outputEvent(final String[] keys, final Object[] values) { - // not thread safe. + // Not thread safe. + if (keys.length == 0) { + return; + } + if (DEBUG) { + if (keys.length != values.length + 1) { + Log.d(TAG, "Key and Value list sizes do not match. " + keys[0]); + } + } try { if (mJsonWriter == NULL_JSON_WRITER) { mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); - mJsonWriter.setLenient(true); mJsonWriter.beginArray(); + mHasWrittenData = true; } mJsonWriter.beginObject(); mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); @@ -283,8 +221,8 @@ public class ResearchLog { for (int i = 0; i < length; i++) { mJsonWriter.name(keys[i + 1]); Object value = values[i]; - if (value instanceof String) { - mJsonWriter.value((String) value); + if (value instanceof CharSequence) { + mJsonWriter.value(value.toString()); } else if (value instanceof Number) { mJsonWriter.value((Number) value); } else if (value instanceof Boolean) { @@ -331,14 +269,11 @@ public class ResearchLog { SuggestedWords words = (SuggestedWords) value; mJsonWriter.beginObject(); mJsonWriter.name("typedWordValid").value(words.mTypedWordValid); - mJsonWriter.name("willAutoCorrect") - .value(words.mWillAutoCorrect); + mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect); mJsonWriter.name("isPunctuationSuggestions") - .value(words.mIsPunctuationSuggestions); - mJsonWriter.name("isObsoleteSuggestions") - .value(words.mIsObsoleteSuggestions); - mJsonWriter.name("isPrediction") - .value(words.mIsPrediction); + .value(words.mIsPunctuationSuggestions); + mJsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions); + mJsonWriter.name("isPrediction").value(words.mIsPrediction); mJsonWriter.name("words"); mJsonWriter.beginArray(); final int size = words.size(); @@ -363,8 +298,8 @@ public class ResearchLog { try { mJsonWriter.close(); } catch (IllegalStateException e1) { - // assume that this is just the json not being terminated properly. - // ignore + // Assume that this is just the json not being terminated properly. + // Ignore } catch (IOException e1) { e1.printStackTrace(); } finally { diff --git a/java/src/com/android/inputmethod/research/ResearchLogUploader.java b/java/src/com/android/inputmethod/research/ResearchLogUploader.java index 3b1213009..9904a1de2 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogUploader.java +++ b/java/src/com/android/inputmethod/research/ResearchLogUploader.java @@ -27,7 +27,6 @@ import android.os.BatteryManager; import android.util.Log; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.R.string; import java.io.BufferedReader; import java.io.File; @@ -48,6 +47,7 @@ public final class ResearchLogUploader { private static final String TAG = ResearchLogUploader.class.getSimpleName(); private static final int UPLOAD_INTERVAL_IN_MS = 1000 * 60 * 15; // every 15 min private static final int BUF_SIZE = 1024 * 8; + protected static final int TIMEOUT_IN_MS = 1000 * 4; private final boolean mCanUpload; private final Context mContext; @@ -55,8 +55,6 @@ public final class ResearchLogUploader { private final URL mUrl; private final ScheduledExecutorService mExecutor; - private Runnable doUploadRunnable = new UploadRunnable(null, false); - public ResearchLogUploader(final Context context, final File filesDir) { mContext = context; mFilesDir = filesDir; @@ -93,11 +91,15 @@ public final class ResearchLogUploader { public void start() { if (mCanUpload) { - Log.d(TAG, "scheduling regular uploading"); - mExecutor.scheduleWithFixedDelay(doUploadRunnable, UPLOAD_INTERVAL_IN_MS, - UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS); - } else { - Log.d(TAG, "no permission to upload"); + mExecutor.scheduleWithFixedDelay(new UploadRunnable(null /* logToWaitFor */, + null /* callback */, false /* forceUpload */), + UPLOAD_INTERVAL_IN_MS, UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS); + } + } + + public void uploadAfterCompletion(final ResearchLog researchLog, final Callback callback) { + if (mCanUpload) { + mExecutor.submit(new UploadRunnable(researchLog, callback, true /* forceUpload */)); } } @@ -106,7 +108,8 @@ public final class ResearchLogUploader { // another upload happening right now, as it may have missed the latest changes. // TODO: Reschedule regular upload tests starting from now. if (mCanUpload) { - mExecutor.submit(new UploadRunnable(callback, true)); + mExecutor.submit(new UploadRunnable(null /* logToWaitFor */, callback, + true /* forceUpload */)); } } @@ -130,19 +133,33 @@ public final class ResearchLogUploader { } class UploadRunnable implements Runnable { + private final ResearchLog mLogToWaitFor; private final Callback mCallback; private final boolean mForceUpload; - public UploadRunnable(final Callback callback, final boolean forceUpload) { + public UploadRunnable(final ResearchLog logToWaitFor, final Callback callback, + final boolean forceUpload) { + mLogToWaitFor = logToWaitFor; mCallback = callback; mForceUpload = forceUpload; } @Override public void run() { + if (mLogToWaitFor != null) { + waitFor(mLogToWaitFor); + } doUpload(); } + private void waitFor(final ResearchLog researchLog) { + try { + researchLog.awaitTermination(TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + private void doUpload() { if (!mForceUpload && (!isExternallyPowered() || !hasWifiConnection())) { return; diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 68bd98a23..77c498648 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -34,15 +34,18 @@ import android.graphics.Paint.Style; import android.inputmethodservice.InputMethodService; import android.os.Build; import android.os.IBinder; +import android.os.SystemClock; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.Button; @@ -64,11 +67,8 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Locale; import java.util.UUID; @@ -94,24 +94,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); private static final boolean IS_SHOWING_INDICATOR = true; private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false; + public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; // constants related to specific log points private static final String WHITESPACE_SEPARATORS = " \t\n\r"; private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; - private static final int ABORT_TIMEOUT_IN_MS = 10 * 1000; // timeout to notify user private static final ResearchLogger sInstance = new ResearchLogger(); // to write to a different filename, e.g., for testing, set mFile before calling start() /* package */ File mFilesDir; /* package */ String mUUIDString; /* package */ ResearchLog mMainResearchLog; - // The mIntentionalResearchLog records all events for the session, private or not (excepting - // passwords). It is written to permanent storage only if the user explicitly commands - // the system to do so. - /* package */ ResearchLog mIntentionalResearchLog; - // LogUnits are queued here and released only when the user requests the intentional log. - private List<LogUnit> mIntentionalResearchLogQueue = new ArrayList<LogUnit>(); + /* package */ ResearchLog mFeedbackLog; + /* package */ MainLogBuffer mMainLogBuffer; + /* package */ LogBuffer mFeedbackLogBuffer; private boolean mIsPasswordView = false; private boolean mIsLoggingSuspended = false; @@ -135,10 +132,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private Dictionary mDictionary; private KeyboardSwitcher mKeyboardSwitcher; private InputMethodService mInputMethodService; - + private final Statistics mStatistics; private ResearchLogUploader mResearchLogUploader; + private LogUnit mCurrentLogUnit = new LogUnit(); + private ResearchLogger() { + mStatistics = Statistics.getInstance(); } public static ResearchLogger getInstance() { @@ -259,6 +259,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang e.apply(); } + private void setLoggingAllowed(boolean enableLogging) { + if (mPrefs == null) { + return; + } + Editor e = mPrefs.edit(); + e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); + e.apply(); + sIsLogging = enableLogging; + } + private File createLogFile(File filesDir) { final StringBuilder sb = new StringBuilder(); sb.append(FILENAME_PREFIX).append('-'); @@ -268,10 +278,35 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return new File(filesDir, sb.toString()); } + private void checkForEmptyEditor() { + if (mInputMethodService == null) { + return; + } + final InputConnection ic = mInputMethodService.getCurrentInputConnection(); + if (ic == null) { + return; + } + final CharSequence textBefore = ic.getTextBeforeCursor(1, 0); + if (!TextUtils.isEmpty(textBefore)) { + mStatistics.setIsEmptyUponStarting(false); + return; + } + final CharSequence textAfter = ic.getTextAfterCursor(1, 0); + if (!TextUtils.isEmpty(textAfter)) { + mStatistics.setIsEmptyUponStarting(false); + return; + } + if (textBefore != null && textAfter != null) { + mStatistics.setIsEmptyUponStarting(true); + } + } + private void start() { maybeShowSplashScreen(); updateSuspendedState(); requestIndicatorRedraw(); + mStatistics.reset(); + checkForEmptyEditor(); if (!isAllowedToLog()) { // Log.w(TAG, "not in usability mode; not logging"); return; @@ -280,73 +315,58 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); return; } - try { - if (mMainResearchLog == null || !mMainResearchLog.isAlive()) { - mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); - } - mMainResearchLog.start(); - if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) { - mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); - } - mIntentionalResearchLog.start(); - } catch (IOException e) { - Log.w(TAG, "Could not start ResearchLogger."); + if (mMainLogBuffer == null) { + mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); + mMainLogBuffer = new MainLogBuffer(mMainResearchLog); + mMainLogBuffer.setSuggest(mSuggest); + } + if (mFeedbackLogBuffer == null) { + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); + // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold + // the feedback LogUnit itself. + mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1); } } /* package */ void stop() { - if (mMainResearchLog != null) { - mMainResearchLog.stop(); - } - if (mIntentionalResearchLog != null) { - mIntentionalResearchLog.stop(); - } - } + logStatistics(); + commitCurrentLogUnit(); - private void setLoggingAllowed(boolean enableLogging) { - if (mPrefs == null) { - return; + if (mMainLogBuffer != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */); + mMainResearchLog.close(); + mMainLogBuffer = null; + } + if (mFeedbackLogBuffer != null) { + mFeedbackLog.close(); + mFeedbackLogBuffer = null; } - Editor e = mPrefs.edit(); - e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); - e.apply(); - sIsLogging = enableLogging; } public boolean abort() { boolean didAbortMainLog = false; - if (mMainResearchLog != null) { - mMainResearchLog.abort(); + if (mMainLogBuffer != null) { + mMainLogBuffer.clear(); try { - mMainResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + didAbortMainLog = mMainResearchLog.blockingAbort(); } catch (InterruptedException e) { - // interrupted early. carry on. + // Don't know whether this succeeded or not. We assume not; this is reported + // to the caller. } - if (mMainResearchLog.isAbortSuccessful()) { - didAbortMainLog = true; - } - mMainResearchLog = null; + mMainLogBuffer = null; } - boolean didAbortIntentionalLog = false; - if (mIntentionalResearchLog != null) { - mIntentionalResearchLog.abort(); + boolean didAbortFeedbackLog = false; + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.clear(); try { - mIntentionalResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + didAbortFeedbackLog = mFeedbackLog.blockingAbort(); } catch (InterruptedException e) { - // interrupted early. carry on. - } - if (mIntentionalResearchLog.isAbortSuccessful()) { - didAbortIntentionalLog = true; + // Don't know whether this succeeded or not. We assume not; this is reported + // to the caller. } - mIntentionalResearchLog = null; - } - return didAbortMainLog && didAbortIntentionalLog; - } - - /* package */ void flush() { - if (mMainResearchLog != null) { - mMainResearchLog.flush(); + mFeedbackLogBuffer = null; } + return didAbortMainLog && didAbortFeedbackLog; } private void restart() { @@ -450,79 +470,39 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); } - private ResearchLog mFeedbackLog; - private List<LogUnit> mFeedbackQueue; - private ResearchLog mSavedMainResearchLog; - private ResearchLog mSavedIntentionalResearchLog; - private List<LogUnit> mSavedIntentionalResearchLogQueue; - - private void saveLogsForFeedback() { - mFeedbackLog = mIntentionalResearchLog; - if (mIntentionalResearchLogQueue != null) { - mFeedbackQueue = new ArrayList<LogUnit>(mIntentionalResearchLogQueue); - } else { - mFeedbackQueue = null; - } - mSavedMainResearchLog = mMainResearchLog; - mSavedIntentionalResearchLog = mIntentionalResearchLog; - mSavedIntentionalResearchLogQueue = mIntentionalResearchLogQueue; - - mMainResearchLog = null; - mIntentionalResearchLog = null; - mIntentionalResearchLogQueue = new ArrayList<LogUnit>(); - } - - private static final int LOG_DRAIN_TIMEOUT_IN_MS = 1000 * 5; + private static final String[] EVENTKEYS_FEEDBACK = { + "UserTimestamp", "contents" + }; public void sendFeedback(final String feedbackContents, final boolean includeHistory) { - if (includeHistory && mFeedbackLog != null) { - try { - LogUnit headerLogUnit = new LogUnit(); - headerLogUnit.addLogAtom(EVENTKEYS_INTENTIONAL_LOG, EVENTKEYS_NULLVALUES, false); - mFeedbackLog.publishAllEvents(headerLogUnit); - for (LogUnit logUnit : mFeedbackQueue) { - mFeedbackLog.publishAllEvents(logUnit); - } - userFeedback(mFeedbackLog, feedbackContents); - mFeedbackLog.stop(); - try { - mFeedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); - mIntentionalResearchLog.start(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - mIntentionalResearchLogQueue.clear(); - } - mResearchLogUploader.uploadNow(null); - } else { - // create a separate ResearchLog just for feedback - final ResearchLog feedbackLog = new ResearchLog(createLogFile(mFilesDir)); - try { - feedbackLog.start(); - userFeedback(feedbackLog, feedbackContents); - feedbackLog.stop(); - feedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS); - mResearchLogUploader.uploadNow(null); - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } + if (mFeedbackLogBuffer == null) { + return; + } + if (!includeHistory) { + mFeedbackLogBuffer.clear(); } + commitCurrentLogUnit(); + final LogUnit feedbackLogUnit = new LogUnit(); + final Object[] values = { + feedbackContents + }; + feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values, + false /* isPotentiallyPrivate */); + mFeedbackLogBuffer.shiftIn(feedbackLogUnit); + publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); + mFeedbackLog.close(); + mResearchLogUploader.uploadAfterCompletion(mFeedbackLog, null); + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); } public void onLeavingSendFeedbackDialog() { mInFeedbackDialog = false; - mMainResearchLog = mSavedMainResearchLog; - mIntentionalResearchLog = mSavedIntentionalResearchLog; - mIntentionalResearchLogQueue = mSavedIntentionalResearchLogQueue; } public void initSuggest(Suggest suggest) { mSuggest = suggest; + if (mMainLogBuffer != null) { + mMainLogBuffer.setSuggest(mSuggest); + } } private void setIsPasswordView(boolean isPasswordView) { @@ -530,7 +510,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { - return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging; + return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; } public void requestIndicatorRedraw() { @@ -577,13 +557,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - private static final String CURRENT_TIME_KEY = "_ct"; - private static final String UPTIME_KEY = "_ut"; - private static final String EVENT_TYPE_KEY = "_ty"; private static final Object[] EVENTKEYS_NULLVALUES = {}; - private LogUnit mCurrentLogUnit = new LogUnit(); - /** * Buffer a research log event, flagging it as privacy-sensitive. * @@ -599,10 +574,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogAtom(keys, values, true); + mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */); } } + private void setCurrentLogUnitContainsDigitFlag() { + mCurrentLogUnit.setContainsDigit(); + } + /** * Buffer a research log event, flaggint it as not privacy-sensitive. * @@ -618,139 +597,54 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private synchronized void enqueueEvent(final String[] keys, final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogAtom(keys, values, false); + mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */); } } - // Used to track how often words are logged. Too-frequent logging can leak - // semantics, disclosing private data. - /* package for test */ static class LoggingFrequencyState { - private static final int DEFAULT_WORD_LOG_FREQUENCY = 10; - private int mWordsRemainingToSkip; - private final int mFrequency; - - /** - * Tracks how often words may be uploaded. - * - * @param frequency 1=Every word, 2=Every other word, etc. - */ - public LoggingFrequencyState(int frequency) { - mFrequency = frequency; - mWordsRemainingToSkip = mFrequency; - } - - public void onWordLogged() { - mWordsRemainingToSkip = mFrequency; - } - - public void onWordNotLogged() { - if (mWordsRemainingToSkip > 1) { - mWordsRemainingToSkip--; + /* package for test */ void commitCurrentLogUnit() { + if (!mCurrentLogUnit.isEmpty()) { + if (mMainLogBuffer != null) { + mMainLogBuffer.shiftIn(mCurrentLogUnit); + if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, + true /* isIncludingPrivateData */); + mMainLogBuffer.resetWordCounter(); + } } + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); + } + mCurrentLogUnit = new LogUnit(); + Log.d(TAG, "commitCurrentLogUnit"); } + } - public boolean isSafeToLog() { - return mWordsRemainingToSkip <= 1; + /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, + final ResearchLog researchLog, final boolean isIncludingPrivateData) { + LogUnit logUnit; + while ((logUnit = logBuffer.shiftOut()) != null) { + researchLog.publish(logUnit, isIncludingPrivateData); } } - /* package for test */ LoggingFrequencyState mLoggingFrequencyState = - new LoggingFrequencyState(LoggingFrequencyState.DEFAULT_WORD_LOG_FREQUENCY); - - /* package for test */ boolean isPrivacyThreat(String word) { - // Current checks: - // - Word not in dictionary - // - Word contains numbers - // - Privacy-safe word not logged recently - if (TextUtils.isEmpty(word)) { - return false; - } - if (!mLoggingFrequencyState.isSafeToLog()) { - return true; - } + private boolean hasOnlyLetters(final String word) { final int length = word.length(); - boolean hasLetter = false; for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = Character.codePointAt(word, i); - if (Character.isDigit(codePoint)) { - return true; + final int codePoint = word.codePointAt(i); + if (!Character.isLetter(codePoint)) { + return false; } - if (Character.isLetter(codePoint)) { - hasLetter = true; - break; // Word may contain digits, but will only be allowed if in the dictionary. - } - } - if (hasLetter) { - if (mDictionary == null && mSuggest != null && mSuggest.hasMainDictionary()) { - mDictionary = mSuggest.getMainDictionary(); - } - if (mDictionary == null) { - // Can't access dictionary. Assume privacy threat. - return true; - } - return !(mDictionary.isValidWord(word)); } - // No letters, no numbers. Punctuation, space, or something else. - return false; + return true; } - private void onWordComplete(String word) { - if (isPrivacyThreat(word)) { - publishLogUnit(mCurrentLogUnit, true); - mLoggingFrequencyState.onWordNotLogged(); - } else { - publishLogUnit(mCurrentLogUnit, false); - mLoggingFrequencyState.onWordLogged(); - } - mCurrentLogUnit = new LogUnit(); - } - - private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) { - if (!isAllowedToLog()) { - return; - } - if (mMainResearchLog == null) { - return; - } - if (isPrivacySensitive) { - mMainResearchLog.publishPublicEvents(logUnit); - } else { - mMainResearchLog.publishAllEvents(logUnit); - } - mIntentionalResearchLogQueue.add(logUnit); - } - - /* package */ void publishCurrentLogUnit(ResearchLog researchLog, boolean isPrivacySensitive) { - publishLogUnit(mCurrentLogUnit, isPrivacySensitive); - } - - static class LogUnit { - private final List<String[]> mKeysList = new ArrayList<String[]>(); - private final List<Object[]> mValuesList = new ArrayList<Object[]>(); - private final List<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>(); - - private void addLogAtom(final String[] keys, final Object[] values, - final Boolean isPotentiallyPrivate) { - mKeysList.add(keys); - mValuesList.add(values); - mIsPotentiallyPrivate.add(isPotentiallyPrivate); - } - - public void publishPublicEventsTo(ResearchLog researchLog) { - final int size = mKeysList.size(); - for (int i = 0; i < size; i++) { - if (!mIsPotentiallyPrivate.get(i)) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); - } - } - } - - public void publishAllEventsTo(ResearchLog researchLog) { - final int size = mKeysList.size(); - for (int i = 0; i < size; i++) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); - } + private void onWordComplete(final String word) { + Log.d(TAG, "onWordComplete: " + word); + if (word != null && word.length() > 0 && hasOnlyLetters(word)) { + mCurrentLogUnit.setWord(word); + mStatistics.recordWordEntered(); } + commitCurrentLogUnit(); } private static int scrubDigitFromCodePoint(int codePoint) { @@ -803,12 +697,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return WORD_REPLACEMENT_STRING; } - // Special methods related to startup, shutdown, logging itself - - private static final String[] EVENTKEYS_INTENTIONAL_LOG = { - "IntentionalLog" - }; - private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" @@ -816,9 +704,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); - if (researchLogger.mInFeedbackDialog) { - researchLogger.saveLogsForFeedback(); - } researchLogger.start(); if (editorInfo != null) { final Context context = researchLogger.mInputMethodService; @@ -846,34 +731,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang stop(); } - private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = { - "LatinIMECommitText", "typedWord" - }; - - public static void latinIME_commitText(final CharSequence typedWord) { - final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); - final Object[] values = { - scrubbedWord - }; - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values); - researchLogger.onWordComplete(scrubbedWord); - } - private static final String[] EVENTKEYS_USER_FEEDBACK = { "UserFeedback", "FeedbackContents" }; - private void userFeedback(ResearchLog researchLog, String feedbackContents) { - // this method is special; it directs the feedbackContents to a particular researchLog - final LogUnit logUnit = new LogUnit(); - final Object[] values = { - feedbackContents - }; - logUnit.addLogAtom(EVENTKEYS_USER_FEEDBACK, values, false); - researchLog.publishAllEvents(logUnit); - } - // Regular logging methods private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = { @@ -908,51 +769,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang "LatinIMEOnCodeInput", "code", "x", "y" }; public static void latinIME_onCodeInput(final int code, final int x, final int y) { - final Object[] values = { - Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y - }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); - } - - private static final String[] EVENTKEYS_CORRECTION = { - "LogCorrection", "subgroup", "before", "after", "position" - }; - public static void logCorrection(final String subgroup, final String before, final String after, - final int position) { - final Object[] values = { - subgroup, scrubDigitsFromString(before), scrubDigitsFromString(after), position - }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_CORRECTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION = { - "LatinIMECommitCurrentAutoCorrection", "typedWord", "autoCorrection" - }; - public static void latinIME_commitCurrentAutoCorrection(final String typedWord, - final String autoCorrection) { - final Object[] values = { - scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection) - }; + final long time = SystemClock.uptimeMillis(); final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent( - EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = { - "LatinIMEDeleteSurroundingText", "length" - }; - public static void latinIME_deleteSurroundingText(final int length) { final Object[] values = { - length + Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y }; - getInstance().enqueueEvent(EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT, values); - } - - private static final String[] EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD = { - "LatinIMEDoubleSpaceAutoPeriod" - }; - public static void latinIME_doubleSpaceAutoPeriod() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD, EVENTKEYS_NULLVALUES); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); + if (Character.isDigit(code)) { + researchLogger.setCurrentLogUnitContainsDigitFlag(); + } + researchLogger.mStatistics.recordChar(code, time); } private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = { @@ -979,6 +805,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void latinIME_onWindowHidden(final int savedSelectionStart, final int savedSelectionEnd, final InputConnection ic) { if (ic != null) { + // Capture the TextView contents. This will trigger onUpdateSelection(), so we + // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, + // it can tell that it was generated by the logging code, and not by the user, and + // therefore keep user-visible state as is. ic.beginBatchEdit(); ic.performContextMenuAction(android.R.id.selectAll); CharSequence charSequence = ic.getSelectedText(0); @@ -1013,9 +843,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values); - // Play it safe. Remove privacy-sensitive events. - researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true); - researchLogger.mCurrentLogUnit = new LogUnit(); + researchLogger.commitCurrentLogUnit(); getInstance().stop(); } } @@ -1048,29 +876,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values); } - private static final String[] EVENTKEYS_LATINIME_PERFORMEDITORACTION = { - "LatinIMEPerformEditorAction", "imeActionNext" - }; - public static void latinIME_performEditorAction(final int imeActionNext) { - final Object[] values = { - imeActionNext - }; - getInstance().enqueueEvent(EVENTKEYS_LATINIME_PERFORMEDITORACTION, values); - } - - private static final String[] EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION = { - "LatinIMEPickApplicationSpecifiedCompletion", "index", "text", "x", "y" - }; - public static void latinIME_pickApplicationSpecifiedCompletion(final int index, - final CharSequence cs, int x, int y) { - final Object[] values = { - index, cs, x, y - }; - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueuePotentiallyPrivateEvent( - EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values); - } - private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = { "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y" }; @@ -1096,21 +901,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values); } - private static final String[] EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT = { - "LatinIMERevertDoubleSpaceWhileInBatchEdit" - }; - public static void latinIME_revertDoubleSpaceWhileInBatchEdit() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT, - EVENTKEYS_NULLVALUES); - } - - private static final String[] EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION = { - "LatinIMERevertSwapPunctuation" - }; - public static void latinIME_revertSwapPunctuation() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION, EVENTKEYS_NULLVALUES); - } - private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = { "LatinIMESendKeyCodePoint", "code" }; @@ -1118,15 +908,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { Keyboard.printableCode(scrubDigitFromCodePoint(code)) }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); + if (Character.isDigit(code)) { + researchLogger.setCurrentLogUnitContainsDigitFlag(); + } } - private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT = { - "LatinIMESwapSwapperAndSpaceWhileInBatchEdit" + private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = { + "LatinIMESwapSwapperAndSpace" }; - public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() { - getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT, - EVENTKEYS_NULLVALUES); + public static void latinIME_swapSwapperAndSpace() { + getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES); } private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = { @@ -1245,6 +1038,128 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values); } + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION = { + "RichInputConnectionCommitCompletion", "completionInfo" + }; + public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { + final Object[] values = { + completionInfo + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values); + } + + // Disabled for privacy-protection reasons. Because this event comes after + // richInputConnection_commitText, which is the event used to separate LogUnits, the + // data in this event can be associated with the next LogUnit, revealing information + // about the current word even if it was supposed to be suppressed. The occurrance of + // autocorrection can be determined by examining the difference between the text strings in + // the last call to richInputConnection_setComposingText before + // richInputConnection_commitText, so it's not a data loss. + // TODO: Figure out how to log this event without loss of privacy. + /* + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = { + "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection" + }; + */ + public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) { + /* + final String typedWord = correctionInfo.getOldText().toString(); + final String autoCorrection = correctionInfo.getNewText().toString(); + final Object[] values = { + scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection) + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values); + */ + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = { + "RichInputConnectionCommitText", "typedWord", "newCursorPosition" + }; + public static void richInputConnection_commitText(final CharSequence typedWord, + final int newCursorPosition) { + final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); + final Object[] values = { + scrubbedWord, newCursorPosition + }; + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT, + values); + researchLogger.onWordComplete(scrubbedWord); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = { + "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength" + }; + public static void richInputConnection_deleteSurroundingText(final int beforeLength, + final int afterLength) { + final Object[] values = { + beforeLength, afterLength + }; + getInstance().enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = { + "RichInputConnectionFinishComposingText" + }; + public static void richInputConnection_finishComposingText() { + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT, + EVENTKEYS_NULLVALUES); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION = { + "RichInputConnectionPerformEditorAction", "imeActionNext" + }; + public static void richInputConnection_performEditorAction(final int imeActionNext) { + final Object[] values = { + imeActionNext + }; + getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION, values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT = { + "RichInputConnectionSendKeyEvent", "eventTime", "action", "code" + }; + public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { + final Object[] values = { + keyEvent.getEventTime(), + keyEvent.getAction(), + keyEvent.getKeyCode() + }; + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, + values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = { + "RichInputConnectionSetComposingText", "text", "newCursorPosition" + }; + public static void richInputConnection_setComposingText(final CharSequence text, + final int newCursorPosition) { + if (text == null) { + throw new RuntimeException("setComposingText is null"); + } + final Object[] values = { + text, newCursorPosition + }; + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, + values); + } + + private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = { + "RichInputConnectionSetSelection", "from", "to" + }; + public static void richInputConnection_setSelection(final int from, final int to) { + final Object[] values = { + from, to + }; + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, + values); + } + private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = { "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent" }; @@ -1277,4 +1192,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public void userTimestamp() { getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES); } + + private static final String[] EVENTKEYS_STATISTICS = { + "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount", + "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys", + "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete" + }; + private static void logStatistics() { + final ResearchLogger researchLogger = getInstance(); + final Statistics statistics = researchLogger.mStatistics; + final Object[] values = { + statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount, + statistics.mSpaceCount, statistics.mDeleteKeyCount, + statistics.mWordCount, statistics.mIsEmptyUponStarting, + statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), + statistics.mBeforeDeleteKeyCounter.getAverageTime(), + statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), + statistics.mAfterDeleteKeyCounter.getAverageTime() + }; + researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values); + } } diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java new file mode 100644 index 000000000..eab465aa2 --- /dev/null +++ b/java/src/com/android/inputmethod/research/Statistics.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 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.research; + +import com.android.inputmethod.keyboard.Keyboard; + +public class Statistics { + // Number of characters entered during a typing session + int mCharCount; + // Number of letter characters entered during a typing session + int mLetterCount; + // Number of number characters entered + int mNumberCount; + // Number of space characters entered + int mSpaceCount; + // Number of delete operations entered (taps on the backspace key) + int mDeleteKeyCount; + // Number of words entered during a session. + int mWordCount; + // Whether the text field was empty upon editing + boolean mIsEmptyUponStarting; + boolean mIsEmptinessStateKnown; + + // Timers to count average time to enter a key, first press a delete key, + // between delete keys, and then to return typing after a delete key. + final AverageTimeCounter mKeyCounter = new AverageTimeCounter(); + final AverageTimeCounter mBeforeDeleteKeyCounter = new AverageTimeCounter(); + final AverageTimeCounter mDuringRepeatedDeleteKeysCounter = new AverageTimeCounter(); + final AverageTimeCounter mAfterDeleteKeyCounter = new AverageTimeCounter(); + + static class AverageTimeCounter { + int mCount; + int mTotalTime; + + public void reset() { + mCount = 0; + mTotalTime = 0; + } + + public void add(long deltaTime) { + mCount++; + mTotalTime += deltaTime; + } + + public int getAverageTime() { + if (mCount == 0) { + return 0; + } + return mTotalTime / mCount; + } + } + + // To account for the interruptions when the user's attention is directed elsewhere, times + // longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic. + public static final int MIN_TYPING_INTERMISSION = 2 * 1000; // in milliseconds + public static final int MIN_DELETION_INTERMISSION = 10 * 1000; // in milliseconds + + // The last time that a tap was performed + private long mLastTapTime; + // The type of the last keypress (delete key or not) + boolean mIsLastKeyDeleteKey; + + private static final Statistics sInstance = new Statistics(); + + public static Statistics getInstance() { + return sInstance; + } + + private Statistics() { + reset(); + } + + public void reset() { + mCharCount = 0; + mLetterCount = 0; + mNumberCount = 0; + mSpaceCount = 0; + mDeleteKeyCount = 0; + mWordCount = 0; + mIsEmptyUponStarting = true; + mIsEmptinessStateKnown = false; + mKeyCounter.reset(); + mBeforeDeleteKeyCounter.reset(); + mDuringRepeatedDeleteKeysCounter.reset(); + mAfterDeleteKeyCounter.reset(); + + mLastTapTime = 0; + mIsLastKeyDeleteKey = false; + } + + public void recordChar(int codePoint, long time) { + final long delta = time - mLastTapTime; + if (codePoint == Keyboard.CODE_DELETE) { + mDeleteKeyCount++; + if (delta < MIN_DELETION_INTERMISSION) { + if (mIsLastKeyDeleteKey) { + mDuringRepeatedDeleteKeysCounter.add(delta); + } else { + mBeforeDeleteKeyCounter.add(delta); + } + } + mIsLastKeyDeleteKey = true; + } else { + mCharCount++; + if (Character.isDigit(codePoint)) { + mNumberCount++; + } + if (Character.isLetter(codePoint)) { + mLetterCount++; + } + if (Character.isSpaceChar(codePoint)) { + mSpaceCount++; + } + if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) { + mAfterDeleteKeyCounter.add(delta); + } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) { + mKeyCounter.add(delta); + } + mIsLastKeyDeleteKey = false; + } + mLastTapTime = time; + } + + public void recordWordEntered() { + mWordCount++; + } + + public void setIsEmptyUponStarting(final boolean isEmpty) { + mIsEmptyUponStarting = isEmpty; + mIsEmptinessStateKnown = true; + } +} |