diff options
Diffstat (limited to 'java/src')
25 files changed, 665 insertions, 402 deletions
diff --git a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java index 8a2818508..c937eeeaa 100644 --- a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java +++ b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java @@ -41,6 +41,8 @@ public final class CursorAnchorInfoCompatWrapper { // Note that CursorAnchorInfo has been introduced in API level XX (Build.VERSION_CODE.LXX). private static final CompatUtils.ClassWrapper sCursorAnchorInfoClass; + private static final CompatUtils.ToIntMethodWrapper sGetSelectionStartMethod; + private static final CompatUtils.ToIntMethodWrapper sGetSelectionEndMethod; private static final CompatUtils.ToObjectMethodWrapper<RectF> sGetCharacterBoundsMethod; private static final CompatUtils.ToIntMethodWrapper sGetCharacterBoundsFlagsMethod; private static final CompatUtils.ToObjectMethodWrapper<CharSequence> sGetComposingTextMethod; @@ -52,10 +54,14 @@ public final class CursorAnchorInfoCompatWrapper { private static final CompatUtils.ToObjectMethodWrapper<Matrix> sGetMatrixMethod; private static final CompatUtils.ToIntMethodWrapper sGetInsertionMarkerFlagsMethod; - private static int COMPOSING_TEXT_START_DEFAULT = -1; + private static int INVALID_TEXT_INDEX = -1; static { sCursorAnchorInfoClass = CompatUtils.getClassWrapper( "android.view.inputmethod.CursorAnchorInfo"); + sGetSelectionStartMethod = sCursorAnchorInfoClass.getPrimitiveMethod( + "getSelectionStart", INVALID_TEXT_INDEX); + sGetSelectionEndMethod = sCursorAnchorInfoClass.getPrimitiveMethod( + "getSelectionEnd", INVALID_TEXT_INDEX); sGetCharacterBoundsMethod = sCursorAnchorInfoClass.getMethod( "getCharacterBounds", (RectF)null, int.class); sGetCharacterBoundsFlagsMethod = sCursorAnchorInfoClass.getPrimitiveMethod( @@ -63,7 +69,7 @@ public final class CursorAnchorInfoCompatWrapper { sGetComposingTextMethod = sCursorAnchorInfoClass.getMethod( "getComposingText", (CharSequence)null); sGetComposingTextStartMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getComposingTextStart", COMPOSING_TEXT_START_DEFAULT); + "getComposingTextStart", INVALID_TEXT_INDEX); sGetInsertionMarkerBaselineMethod = sCursorAnchorInfoClass.getPrimitiveMethod( "getInsertionMarkerBaseline", 0.0f); sGetInsertionMarkerBottomMethod = sCursorAnchorInfoClass.getPrimitiveMethod( @@ -105,6 +111,14 @@ public final class CursorAnchorInfoCompatWrapper { return FakeHolder.sInstance; } + public int getSelectionStart() { + return sGetSelectionStartMethod.invoke(mInstance); + } + + public int getSelectionEnd() { + return sGetSelectionEndMethod.invoke(mInstance); + } + public CharSequence getComposingText() { return sGetComposingTextMethod.invoke(mInstance); } diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 99a34bec7..863a8b7ad 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -98,6 +98,16 @@ public class Key implements Comparable<Key> { private final int mWidth; /** Height of the key, excluding the gap */ private final int mHeight; + /** + * The combined width in pixels of the horizontal gaps belonging to this key, both to the left + * and to the right. I.e., mWidth + mHorizontalGap = total width belonging to the key. + */ + private final int mHorizontalGap; + /** + * The combined height in pixels of the vertical gaps belonging to this key, both above and + * below. I.e., mHeight + mVerticalGap = total height belonging to the key. + */ + private final int mVerticalGap; /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ private final int mX; /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ @@ -198,8 +208,10 @@ public class Key implements Comparable<Key> { final String hintLabel, final int labelFlags, final int backgroundType, final int x, final int y, final int width, final int height, final int horizontalGap, final int verticalGap) { - mHeight = height - verticalGap; mWidth = width - horizontalGap; + mHeight = height - verticalGap; + mHorizontalGap = horizontalGap; + mVerticalGap = verticalGap; mHintLabel = hintLabel; mLabelFlags = labelFlags; mBackgroundType = backgroundType; @@ -214,7 +226,7 @@ public class Key implements Comparable<Key> { mEnabled = (code != CODE_UNSPECIFIED); mIconId = iconId; // Horizontal gap is divided equally to both sides of the key. - mX = x + horizontalGap / 2; + mX = x + mHorizontalGap / 2; mY = y; mHitBox.set(x, y, x + width + 1, y + height); mKeyVisualAttributes = null; @@ -235,18 +247,21 @@ public class Key implements Comparable<Key> { */ public Key(final String keySpec, final TypedArray keyAttr, final KeyStyle style, final KeyboardParams params, final KeyboardRow row) { - final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap; + mHorizontalGap = isSpacer() ? 0 : params.mHorizontalGap; + mVerticalGap = params.mVerticalGap; + + final float horizontalGapFloat = mHorizontalGap; final int rowHeight = row.getRowHeight(); - mHeight = rowHeight - params.mVerticalGap; + mHeight = rowHeight - mVerticalGap; final float keyXPos = row.getKeyX(keyAttr); final float keyWidth = row.getKeyWidth(keyAttr, keyXPos); final int keyYPos = row.getKeyY(); // Horizontal gap is divided equally to both sides of the key. - mX = Math.round(keyXPos + horizontalGap / 2); + mX = Math.round(keyXPos + horizontalGapFloat / 2); mY = keyYPos; - mWidth = Math.round(keyWidth - horizontalGap); + mWidth = Math.round(keyWidth - horizontalGapFloat); mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1, keyYPos + rowHeight); // Update row to have current x coordinate. @@ -388,6 +403,8 @@ public class Key implements Comparable<Key> { mIconId = key.mIconId; mWidth = key.mWidth; mHeight = key.mHeight; + mHorizontalGap = key.mHorizontalGap; + mVerticalGap = key.mVerticalGap; mX = key.mX; mY = key.mY; mHitBox.set(key.mHitBox); @@ -788,6 +805,24 @@ public class Key implements Comparable<Key> { } /** + * The combined width in pixels of the horizontal gaps belonging to this key, both above and + * below. I.e., getWidth() + getHorizontalGap() = total width belonging to the key. + * @return Horizontal gap belonging to this key. + */ + public int getHorizontalGap() { + return mHorizontalGap; + } + + /** + * The combined height in pixels of the vertical gaps belonging to this key, both above and + * below. I.e., getHeight() + getVerticalGap() = total height belonging to the key. + * @return Vertical gap belonging to this key. + */ + public int getVerticalGap() { + return mVerticalGap; + } + + /** * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap. * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap. */ diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java index 43c61443e..f9cf3535e 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java @@ -77,8 +77,7 @@ public final class KeyboardId { private final int mHashCode; - public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params, - boolean isSplitLayout) { + public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) { mSubtype = params.mSubtype; mLocale = SubtypeLocaleUtils.getSubtypeLocale(mSubtype); mWidth = params.mKeyboardWidth; @@ -91,7 +90,7 @@ public final class KeyboardId { mCustomActionLabel = (mEditorInfo.actionLabel != null) ? mEditorInfo.actionLabel.toString() : null; mHasShortcutKey = params.mVoiceInputKeyEnabled; - mIsSplitLayout = isSplitLayout; + mIsSplitLayout = params.mIsSplitLayoutEnabled; mHashCode = computeHashCode(this); } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index 1dbecdc81..47fb7b320 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -115,6 +115,12 @@ public final class KeyboardLayoutSet { int mKeyboardWidth; int mKeyboardHeight; int mScriptId = ScriptUtils.SCRIPT_LATIN; + // Indicates if the user has enabled the split-layout preference + // and the required ProductionFlags are enabled. + boolean mIsSplitLayoutEnabledByUser; + // Indicates if split layout is actually enabled, taking into account + // whether the user has enabled it, and the keyboard layout supports it. + boolean mIsSplitLayoutEnabled; // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = new SparseArray<>(); @@ -170,9 +176,9 @@ public final class KeyboardLayoutSet { // specified as an elementKeyboard attribute in the file. // The KeyboardId is an internal key for a Keyboard object. - // TODO: AND mSupportsSplitLayout with the user preference that forces a split. - final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams, - elementParams.mSupportsSplitLayout); + mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser + && elementParams.mSupportsSplitLayout; + final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); try { return getKeyboard(elementParams, id); } catch (final RuntimeException e) { @@ -290,12 +296,19 @@ public final class KeyboardLayoutSet { return this; } - public void disableTouchPositionCorrectionData() { + public Builder disableTouchPositionCorrectionData() { mParams.mDisableTouchPositionCorrectionDataForTest = true; + return this; } - public void setScriptId(final int scriptId) { + public Builder setScriptId(final int scriptId) { mParams.mScriptId = scriptId; + return this; + } + + public Builder setSplitLayoutEnabledByUser(final boolean enabled) { + mParams.mIsSplitLayoutEnabledByUser = enabled; + return this; } public KeyboardLayoutSet build() { diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index 6cd7955ce..246d11bac 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -36,6 +36,7 @@ import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.SubtypeSwitcher; import com.android.inputmethod.latin.WordComposer; +import com.android.inputmethod.latin.define.ProductionFlags; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.utils.ResourceUtils; @@ -114,6 +115,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype()); builder.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey); builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey()); + builder.setSplitLayoutEnabledByUser(ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED + && settingsValues.mIsSplitKeyboardEnabled); mKeyboardLayoutSet = builder.build(); try { mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState); diff --git a/java/src/com/android/inputmethod/keyboard/TextDecorator.java b/java/src/com/android/inputmethod/keyboard/TextDecorator.java index f614b2257..315d36313 100644 --- a/java/src/com/android/inputmethod/keyboard/TextDecorator.java +++ b/java/src/com/android/inputmethod/keyboard/TextDecorator.java @@ -17,23 +17,22 @@ package com.android.inputmethod.keyboard; import android.graphics.Matrix; -import android.graphics.PointF; import android.graphics.RectF; import android.inputmethodservice.InputMethodService; import android.os.Message; import android.text.TextUtils; import android.util.Log; import android.view.View; +import android.view.inputmethod.CursorAnchorInfo; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; import javax.annotation.Nonnull; /** - * A controller class of commit/add-to-dictionary indicator (a.k.a. TextDecorator). This class + * A controller class of the add-to-dictionary indicator (a.k.a. TextDecorator). This class * is designed to be independent of UI subsystems such as {@link View}. All the UI related * operations are delegated to {@link TextDecoratorUi} via {@link TextDecoratorUiOperator}. */ @@ -41,18 +40,22 @@ public class TextDecorator { private static final String TAG = TextDecorator.class.getSimpleName(); private static final boolean DEBUG = false; - private static final int MODE_NONE = 0; - private static final int MODE_COMMIT = 1; - private static final int MODE_ADD_TO_DICTIONARY = 2; + private static final int INVALID_CURSOR_INDEX = -1; - private int mMode = MODE_NONE; + private static final int MODE_MONITOR = 0; + private static final int MODE_WAITING_CURSOR_INDEX = 1; + private static final int MODE_SHOWING_INDICATOR = 2; - private final PointF mLocalOrigin = new PointF(); - private final RectF mRelativeIndicatorBounds = new RectF(); - private final RectF mRelativeComposingTextBounds = new RectF(); + private int mMode = MODE_MONITOR; + + private String mLastComposingText = null; + private RectF mIndicatorBoundsForLastComposingText = new RectF(); + private RectF mComposingTextBoundsForLastComposingText = new RectF(); private boolean mIsFullScreenMode = false; - private SuggestedWordInfo mWaitingWord = null; + private String mWaitingWord = null; + private int mWaitingCursorStart = INVALID_CURSOR_INDEX; + private int mWaitingCursorEnd = INVALID_CURSOR_INDEX; private CursorAnchorInfoCompatWrapper mCursorAnchorInfoWrapper = null; @Nonnull @@ -63,16 +66,10 @@ public class TextDecorator { public interface Listener { /** - * Called when the user clicks the composing text to commit. - * @param wordInfo the suggested word which the user clicked on. + * Called when the user clicks the indicator to add the word into the dictionary. + * @param word the word which the user clicked on. */ - void onClickComposingTextToCommit(final SuggestedWordInfo wordInfo); - - /** - * Called when the user clicks the composing text to add the word into the dictionary. - * @param wordInfo the suggested word which the user clicked on. - */ - void onClickComposingTextToAddToDictionary(final SuggestedWordInfo wordInfo); + void onClickComposingTextToAddToDictionary(final String word); } public TextDecorator(final Listener listener) { @@ -103,46 +100,19 @@ public class TextDecorator { } /** - * Shows the "Commit" indicator and associates it with the given suggested word. - * - * <p>The effect of {@link #showCommitIndicator(SuggestedWordInfo)} and - * {@link #showAddToDictionaryIndicator(SuggestedWordInfo)} are exclusive to each other. Call - * {@link #reset()} to hide the indicator.</p> - * - * @param wordInfo the suggested word which should be associated with the indicator. This object - * will be passed back in {@link Listener#onClickComposingTextToCommit(SuggestedWordInfo)} - */ - public void showCommitIndicator(final SuggestedWordInfo wordInfo) { - if (mMode == MODE_COMMIT && wordInfo != null && - TextUtils.equals(mWaitingWord.mWord, wordInfo.mWord)) { - // Skip layout for better performance. - return; - } - mWaitingWord = wordInfo; - mMode = MODE_COMMIT; - layoutLater(); - } - - /** - * Shows the "Add to dictionary" indicator and associates it with associating the given - * suggested word. - * - * <p>The effect of {@link #showCommitIndicator(SuggestedWordInfo)} and - * {@link #showAddToDictionaryIndicator(SuggestedWordInfo)} are exclusive to each other. Call - * {@link #reset()} to hide the indicator.</p> + * Shows the "Add to dictionary" indicator and associates it with associating the given word. * - * @param wordInfo the suggested word which should be associated with the indicator. This object - * will be passed back in - * {@link Listener#onClickComposingTextToAddToDictionary(SuggestedWordInfo)}. + * @param word the word which should be associated with the indicator. This object will be + * passed back in {@link Listener#onClickComposingTextToAddToDictionary(String)}. + * @param selectionStart the cursor index (inclusive) when the indicator should be displayed. + * @param selectionEnd the cursor index (exclusive) when the indicator should be displayed. */ - public void showAddToDictionaryIndicator(final SuggestedWordInfo wordInfo) { - if (mMode == MODE_ADD_TO_DICTIONARY && wordInfo != null && - TextUtils.equals(mWaitingWord.mWord, wordInfo.mWord)) { - // Skip layout for better performance. - return; - } - mWaitingWord = wordInfo; - mMode = MODE_ADD_TO_DICTIONARY; + public void showAddToDictionaryIndicator(final String word, final int selectionStart, + final int selectionEnd) { + mWaitingWord = word; + mWaitingCursorStart = selectionStart; + mWaitingCursorEnd = selectionEnd; + mMode = MODE_WAITING_CURSOR_INDEX; layoutLater(); return; } @@ -165,18 +135,19 @@ public class TextDecorator { */ public void reset() { mWaitingWord = null; - mMode = MODE_NONE; - mLocalOrigin.set(0.0f, 0.0f); - mRelativeIndicatorBounds.set(0.0f, 0.0f, 0.0f, 0.0f); - mRelativeComposingTextBounds.set(0.0f, 0.0f, 0.0f, 0.0f); + mMode = MODE_MONITOR; + mWaitingCursorStart = INVALID_CURSOR_INDEX; + mWaitingCursorEnd = INVALID_CURSOR_INDEX; cancelLayoutInternalExpectedly("Resetting internal state."); } /** - * Must be called when the {@link InputMethodService#onUpdateCursorAnchorInfo()} is called. + * Must be called when the {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} + * is called. * * <p>CAVEAT: Currently the input method author is responsible for ignoring - * {@link InputMethodService#onUpdateCursorAnchorInfo()} called in full screen mode.</p> + * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} called in full screen + * mode.</p> * @param info the compatibility wrapper object for the received {@link CursorAnchorInfo}. */ public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { @@ -185,29 +156,6 @@ public class TextDecorator { layoutImmediately(); } - /** - * Hides indicator if the new composing text doesn't match the expected one. - * - * <p>Calling this method is optional but recommended whenever the new composition is passed to - * the application. The motivation of this method is to reduce the UI latency. With this method, - * we can hide the indicator without waiting the arrival of the - * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} callback, assuming that - * the application accepts the new composing text without any modification. Even if this - * assumption is false, the indicator will be shown again when - * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is actually received. - * </p> - * - * @param newComposingText the new composing text that is being passed to the application. - */ - public void hideIndicatorIfNecessary(final CharSequence newComposingText) { - if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) { - return; - } - if (!TextUtils.equals(newComposingText, mWaitingWord.mWord)) { - mUiOperator.hideUi(); - } - } - private void cancelLayoutInternalUnexpectedly(final String message) { mUiOperator.hideUi(); Log.d(TAG, message); @@ -232,15 +180,6 @@ public class TextDecorator { } private void layoutMain() { - if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) { - if (mMode == MODE_NONE) { - cancelLayoutInternalExpectedly("Not ready for layouting."); - } else { - cancelLayoutInternalUnexpectedly("Unknown mMode=" + mMode); - } - return; - } - final CursorAnchorInfoCompatWrapper info = mCursorAnchorInfoWrapper; if (info == null) { @@ -254,104 +193,117 @@ public class TextDecorator { } final CharSequence composingText = info.getComposingText(); - if (mMode == MODE_COMMIT) { - if (composingText == null) { - cancelLayoutInternalExpectedly("composingText is null."); - return; - } + if (!TextUtils.isEmpty(composingText)) { final int composingTextStart = info.getComposingTextStart(); final int lastCharRectIndex = composingTextStart + composingText.length() - 1; final RectF lastCharRect = info.getCharacterBounds(lastCharRectIndex); - final int lastCharRectFlag = info.getCharacterBoundsFlags(lastCharRectIndex); + final int lastCharRectFlags = info.getCharacterBoundsFlags(lastCharRectIndex); final boolean hasInvisibleRegionInLastCharRect = - (lastCharRectFlag & CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION) + (lastCharRectFlags & CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION) != 0; if (lastCharRect == null || matrix == null || hasInvisibleRegionInLastCharRect) { mUiOperator.hideUi(); return; } - final RectF segmentStartCharRect = new RectF(lastCharRect); - for (int i = composingText.length() - 2; i >= 0; --i) { - final RectF charRect = info.getCharacterBounds(composingTextStart + i); - if (charRect == null) { + + // Note that the following layout information is fragile, and must be invalidated + // even when surrounding text next to the composing text is changed because it can + // affect how the composing text is rendered. + // TODO: Investigate if we can change the input logic to make the target text + // composing state so that we can retrieve the character bounds reliably. + final String composingTextString = composingText.toString(); + final float top = lastCharRect.top; + final float bottom = lastCharRect.bottom; + float left = lastCharRect.left; + float right = lastCharRect.right; + boolean useRtlLayout = false; + for (int i = composingText.length() - 1; i >= 0; --i) { + final int characterIndex = composingTextStart + i; + final RectF characterBounds = info.getCharacterBounds(characterIndex); + final int characterBoundsFlags = info.getCharacterBoundsFlags(characterIndex); + if (characterBounds == null) { break; } - if (charRect.top != segmentStartCharRect.top) { + if (characterBounds.top != top) { break; } - if (charRect.bottom != segmentStartCharRect.bottom) { + if (characterBounds.bottom != bottom) { break; } - segmentStartCharRect.set(charRect); - } - - mLocalOrigin.set(lastCharRect.right, lastCharRect.top); - mRelativeIndicatorBounds.set(lastCharRect.right, lastCharRect.top, - lastCharRect.right + lastCharRect.height(), lastCharRect.bottom); - mRelativeIndicatorBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y); - - mRelativeIndicatorBounds.set(lastCharRect.right, lastCharRect.top, - lastCharRect.right + lastCharRect.height(), lastCharRect.bottom); - mRelativeIndicatorBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y); - - mRelativeComposingTextBounds.set(segmentStartCharRect.left, segmentStartCharRect.top, - segmentStartCharRect.right, segmentStartCharRect.bottom); - mRelativeComposingTextBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y); - - if (mWaitingWord == null) { - cancelLayoutInternalExpectedly("mWaitingText is null."); - return; - } - if (TextUtils.isEmpty(mWaitingWord.mWord)) { - cancelLayoutInternalExpectedly("mWaitingText.mWord is empty."); - return; + if ((characterBoundsFlags & CursorAnchorInfoCompatWrapper.FLAG_IS_RTL) != 0) { + // This is for both RTL text and bi-directional text. RTL languages usually mix + // RTL characters with LTR characters and in this case we should display the + // indicator on the left, while in LTR languages that normally never happens. + // TODO: Try to come up with a better algorithm. + useRtlLayout = true; + } + left = Math.min(characterBounds.left, left); + right = Math.max(characterBounds.right, right); } - if (!TextUtils.equals(composingText, mWaitingWord.mWord)) { - // This is indeed an expected situation because of the asynchronous nature of - // input method framework in Android. Note that composingText is notified from the - // application, while mWaitingWord.mWord is obtained directly from the InputLogic. - cancelLayoutInternalExpectedly( - "Composing text doesn't match the one we are waiting for."); - return; + mLastComposingText = composingTextString; + mComposingTextBoundsForLastComposingText.set(left, top, right, bottom); + // The height and width of the indicator is the same as the height of the composing + // text. + final float indicatorSize = bottom - top; + mIndicatorBoundsForLastComposingText.set(0.0f, 0.0f, indicatorSize, indicatorSize); + // The horizontal position of the indicator depends on the text direction. + final float indicatorTop = top; + final float indicatorLeft; + if (useRtlLayout) { + indicatorLeft = left - indicatorSize; + } else { + indicatorLeft = right; } - } else { - if (!mIsFullScreenMode && !TextUtils.isEmpty(composingText)) { - // This is an unexpected case. - // TODO: Document this. + mIndicatorBoundsForLastComposingText.offset(indicatorLeft, indicatorTop); + } + + final int selectionStart = info.getSelectionStart(); + final int selectionEnd = info.getSelectionEnd(); + switch (mMode) { + case MODE_MONITOR: mUiOperator.hideUi(); return; - } - // In MODE_ADD_TO_DICTIONARY, we cannot retrieve the character position at all because - // of the lack of composing text. We will use the insertion marker position instead. - if ((info.getInsertionMarkerFlags() & - CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION) != 0) { - mUiOperator.hideUi(); + case MODE_WAITING_CURSOR_INDEX: + if (selectionStart != mWaitingCursorStart || selectionEnd != mWaitingCursorEnd) { + mUiOperator.hideUi(); + return; + } + mMode = MODE_SHOWING_INDICATOR; + break; + case MODE_SHOWING_INDICATOR: + if (selectionStart != mWaitingCursorStart || selectionEnd != mWaitingCursorEnd) { + mUiOperator.hideUi(); + mMode = MODE_MONITOR; + mWaitingCursorStart = INVALID_CURSOR_INDEX; + mWaitingCursorEnd = INVALID_CURSOR_INDEX; + return; + } + break; + default: + cancelLayoutInternalUnexpectedly("Unexpected internal mode=" + mMode); return; - } - final float insertionMarkerHolizontal = info.getInsertionMarkerHorizontal(); - final float insertionMarkerTop = info.getInsertionMarkerTop(); - mLocalOrigin.set(insertionMarkerHolizontal, insertionMarkerTop); } - final RectF indicatorBounds = new RectF(mRelativeIndicatorBounds); - final RectF composingTextBounds = new RectF(mRelativeComposingTextBounds); - indicatorBounds.offset(mLocalOrigin.x, mLocalOrigin.y); - composingTextBounds.offset(mLocalOrigin.x, mLocalOrigin.y); - mUiOperator.layoutUi(mMode == MODE_COMMIT, matrix, indicatorBounds, composingTextBounds); + if (!TextUtils.equals(mLastComposingText, mWaitingWord)) { + cancelLayoutInternalUnexpectedly("mLastComposingText doesn't match mWaitingWord"); + return; + } + + if ((info.getInsertionMarkerFlags() & + CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION) != 0) { + mUiOperator.hideUi(); + return; + } + + mUiOperator.layoutUi(matrix, mIndicatorBoundsForLastComposingText, + mComposingTextBoundsForLastComposingText); } private void onClickIndicator() { - if (mWaitingWord == null || TextUtils.isEmpty(mWaitingWord.mWord)) { + if (mMode != MODE_SHOWING_INDICATOR) { return; } - switch (mMode) { - case MODE_COMMIT: - mListener.onClickComposingTextToCommit(mWaitingWord); - break; - case MODE_ADD_TO_DICTIONARY: - mListener.onClickComposingTextToAddToDictionary(mWaitingWord); - break; - } + mListener.onClickComposingTextToAddToDictionary(mWaitingWord); } private final LayoutInvalidator mLayoutInvalidator = new LayoutInvalidator(this); @@ -407,10 +359,7 @@ public class TextDecorator { private final static Listener EMPTY_LISTENER = new Listener() { @Override - public void onClickComposingTextToCommit(SuggestedWordInfo wordInfo) { - } - @Override - public void onClickComposingTextToAddToDictionary(SuggestedWordInfo wordInfo) { + public void onClickComposingTextToAddToDictionary(final String word) { } }; @@ -425,8 +374,7 @@ public class TextDecorator { public void setOnClickListener(Runnable listener) { } @Override - public void layoutUi(boolean isCommitMode, Matrix matrix, RectF indicatorBounds, - RectF composingTextBounds) { + public void layoutUi(Matrix matrix, RectF indicatorBounds, RectF composingTextBounds) { } }; } diff --git a/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java b/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java index 6e215a9ca..b67d17789 100644 --- a/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java +++ b/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java @@ -46,7 +46,6 @@ public final class TextDecoratorUi implements TextDecoratorUiOperator { private static final int VISUAL_DEBUG_HIT_AREA_COLOR = 0x80ff8000; private final RelativeLayout mLocalRootView; - private final CommitIndicatorView mCommitIndicatorView; private final AddToDictionaryIndicatorView mAddToDictionaryIndicatorView; private final PopupWindow mTouchEventWindow; private final View mTouchEventWindowClickListenerView; @@ -73,9 +72,7 @@ public final class TextDecoratorUi implements TextDecoratorUiOperator { mLocalRootView.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); final ViewGroup contentView = getContentView(inputView); - mCommitIndicatorView = new CommitIndicatorView(context); mAddToDictionaryIndicatorView = new AddToDictionaryIndicatorView(context); - mLocalRootView.addView(mCommitIndicatorView); mLocalRootView.addView(mAddToDictionaryIndicatorView); if (contentView != null) { contentView.addView(mLocalRootView); @@ -110,17 +107,15 @@ public final class TextDecoratorUi implements TextDecoratorUiOperator { @Override public void hideUi() { - mCommitIndicatorView.setVisibility(View.GONE); mAddToDictionaryIndicatorView.setVisibility(View.GONE); mTouchEventWindow.dismiss(); } @Override - public void layoutUi(final boolean isCommitMode, final Matrix matrix, - final RectF indicatorBounds, final RectF composingTextBounds) { + public void layoutUi(final Matrix matrix, final RectF indicatorBounds, + final RectF composingTextBounds) { final RectF indicatorBoundsInScreenCoordinates = new RectF(); matrix.mapRect(indicatorBoundsInScreenCoordinates, indicatorBounds); - mCommitIndicatorView.setBounds(indicatorBoundsInScreenCoordinates); mAddToDictionaryIndicatorView.setBounds(indicatorBoundsInScreenCoordinates); final RectF hitAreaBounds = new RectF(composingTextBounds); @@ -133,20 +128,9 @@ public final class TextDecoratorUi implements TextDecoratorUiOperator { mLocalRootView.getLocationOnScreen(originScreen); final int viewOriginX = originScreen[0]; final int viewOriginY = originScreen[1]; - - final View toBeShown; - final View toBeHidden; - if (isCommitMode) { - toBeShown = mCommitIndicatorView; - toBeHidden = mAddToDictionaryIndicatorView; - } else { - toBeShown = mAddToDictionaryIndicatorView; - toBeHidden = mCommitIndicatorView; - } - toBeShown.setX(indicatorBoundsInScreenCoordinates.left - viewOriginX); - toBeShown.setY(indicatorBoundsInScreenCoordinates.top - viewOriginY); - toBeShown.setVisibility(View.VISIBLE); - toBeHidden.setVisibility(View.GONE); + mAddToDictionaryIndicatorView.setX(indicatorBoundsInScreenCoordinates.left - viewOriginX); + mAddToDictionaryIndicatorView.setY(indicatorBoundsInScreenCoordinates.top - viewOriginY); + mAddToDictionaryIndicatorView.setVisibility(View.VISIBLE); if (mTouchEventWindow.isShowing()) { mTouchEventWindow.update((int)hitAreaBoundsInScreenCoordinates.left - viewOriginX, @@ -239,15 +223,6 @@ public final class TextDecoratorUi implements TextDecoratorUiOperator { return windowContentView; } - private static final class CommitIndicatorView extends TextDecoratorUi.IndicatorView { - public CommitIndicatorView(final Context context) { - super(context, R.array.text_decorator_commit_indicator_path, - R.integer.text_decorator_commit_indicator_path_size, - R.color.text_decorator_commit_indicator_background_color, - R.color.text_decorator_commit_indicator_foreground_color); - } - } - private static final class AddToDictionaryIndicatorView extends TextDecoratorUi.IndicatorView { public AddToDictionaryIndicatorView(final Context context) { super(context, R.array.text_decorator_add_to_dictionary_indicator_path, diff --git a/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java b/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java index f84e12d8c..9c0b64ad4 100644 --- a/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java +++ b/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java @@ -44,12 +44,10 @@ public interface TextDecoratorUiOperator { /** * Called when the layout should be updated. - * @param isCommitMode {@code true} if the commit indicator should be shown. Show the - * add-to-dictionary indicator otherwise. * @param matrix The matrix that transforms the local coordinates into the screen coordinates. * @param indicatorBounds The bounding box of the indicator, in local coordinates. * @param composingTextBounds The bounding box of the composing text, in local coordinates. */ - void layoutUi(final boolean isCommitMode, final Matrix matrix, final RectF indicatorBounds, + void layoutUi(final Matrix matrix, final RectF indicatorBounds, final RectF composingTextBounds); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index fa4192790..2056a0b9d 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -674,15 +674,18 @@ public class KeyboardBuilder<KP extends KeyboardParams> { R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage()); final boolean countryCodeMatched = matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry()); + final boolean splitLayoutMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_isSplitLayout, id.mIsSplitLayout); final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched && keyboardThemeMacthed && modeMatched && navigateNextMatched && navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched && isIconDefinedMatched - && localeCodeMatched && languageCodeMatched && countryCodeMatched; + && localeCodeMatched && languageCodeMatched && countryCodeMatched + && splitLayoutMatched; if (DEBUG) { - startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, + startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, textAttr(caseAttr.getString( R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"), textAttr(caseAttr.getString( @@ -707,6 +710,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { "languageSwitchKeyEnabled"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine, "isMultiLine"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_isSplitLayout, + "splitLayout"), textAttr(caseAttr.getString(R.styleable.Keyboard_Case_isIconDefined), "isIconDefined"), textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode), diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index eced45ea5..0f09daf86 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -595,8 +595,9 @@ public class DictionaryFacilitator { final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - final SuggestionResults suggestionResults = - new SuggestionResults(SuggestedWords.MAX_SUGGESTIONS); + final SuggestionResults suggestionResults = new SuggestionResults( + SuggestedWords.MAX_SUGGESTIONS, + prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence); final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT }; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 475782042..69fe6de9a 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -189,9 +189,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6; private static final int MSG_RESET_CACHES = 7; private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8; - private static final int MSG_SHOW_COMMIT_INDICATOR = 9; // Update this when adding new messages - private static final int MSG_LAST = MSG_SHOW_COMMIT_INDICATOR; + private static final int MSG_LAST = MSG_WAIT_FOR_DICTIONARY_LOAD; private static final int ARG1_NOT_GESTURE_INPUT = 0; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; @@ -202,7 +201,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private int mDelayInMillisecondsToUpdateSuggestions; private int mDelayInMillisecondsToUpdateShiftState; - private int mDelayInMillisecondsToShowCommitIndicator; public UIHandler(final LatinIME ownerInstance) { super(ownerInstance); @@ -218,8 +216,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen R.integer.config_delay_in_milliseconds_to_update_suggestions); mDelayInMillisecondsToUpdateShiftState = res.getInteger( R.integer.config_delay_in_milliseconds_to_update_shift_state); - mDelayInMillisecondsToShowCommitIndicator = res.getInteger( - R.integer.text_decorator_delay_in_milliseconds_to_show_commit_indicator); } @Override @@ -277,14 +273,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen latinIme.getCurrentRecapitalizeState()); } break; - case MSG_SHOW_COMMIT_INDICATOR: - // Protocol of MSG_SET_COMMIT_INDICATOR_ENABLED: - // - what: MSG_SHOW_COMMIT_INDICATOR - // - arg1: not used. - // - arg2: not used. - // - obj: the Runnable object to be called back. - ((Runnable) msg.obj).run(); - break; case MSG_WAIT_FOR_DICTIONARY_LOAD: Log.i(TAG, "Timeout waiting for dictionary load"); break; @@ -385,19 +373,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget(); } - /** - * Posts a delayed task to show the commit indicator. - * - * <p>Only one task can exist in the queue. When this method is called, any prior task that - * has not yet fired will be canceled.</p> - * @param task the runnable object that will be fired when the delayed task is dispatched. - */ - public void postShowCommitIndicatorTask(final Runnable task) { - removeMessages(MSG_SHOW_COMMIT_INDICATOR); - sendMessageDelayed(obtainMessage(MSG_SHOW_COMMIT_INDICATOR, task), - mDelayInMillisecondsToShowCommitIndicator); - } - // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccessiveImsCallback; @@ -1516,19 +1491,23 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final boolean isEmptyApplicationSpecifiedCompletions = currentSettingsValues.isApplicationSpecifiedCompletionsOn() && suggestedWords.isEmpty(); - final boolean noSuggestionsToShow = (SuggestedWords.EMPTY == suggestedWords) + final boolean noSuggestionsFromDictionaries = (SuggestedWords.EMPTY == suggestedWords) || suggestedWords.isPunctuationSuggestions() || isEmptyApplicationSpecifiedCompletions; - if (shouldShowImportantNotice && noSuggestionsToShow) { + final boolean isBeginningOfSentencePrediction = (suggestedWords.mInputStyle + == SuggestedWords.INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION); + final boolean noSuggestionsToOverrideImportantNotice = noSuggestionsFromDictionaries + || isBeginningOfSentencePrediction; + if (shouldShowImportantNotice && noSuggestionsToOverrideImportantNotice) { if (mSuggestionStripView.maybeShowImportantNoticeTitle()) { return; } } if (currentSettingsValues.isSuggestionsEnabledPerUserSettings() - // We should clear suggestions if there is no suggestion to show. - || noSuggestionsToShow - || currentSettingsValues.isApplicationSpecifiedCompletionsOn()) { + || currentSettingsValues.isApplicationSpecifiedCompletionsOn() + // We should clear the contextual strip if there is no suggestion from dictionaries. + || noSuggestionsFromDictionaries) { mSuggestionStripView.setSuggestions(suggestedWords, SubtypeLocaleUtils.isRtlLanguage(mSubtypeSwitcher.getCurrentSubtype())); } diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index dc00ecc8f..d672430a1 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -252,7 +252,7 @@ public final class RichInputConnection { * See {@link InputConnection#commitText(CharSequence, int)}. */ public void commitText(final CharSequence text, final int newCursorPosition) { - commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT); + commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT, text.length()); } /** @@ -265,9 +265,11 @@ public final class RichInputConnection { * the background color. Note that this method specifies {@link BackgroundColorSpan} with * {@link Spanned#SPAN_COMPOSING} flag, meaning that the background color persists until * {@link #finishComposingText()} is called. + * @param coloredTextLength the length of text, in Java chars, which should be rendered with + * the given background color. */ public void commitTextWithBackgroundColor(final CharSequence text, final int newCursorPosition, - final int color) { + final int color, final int coloredTextLength) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); mCommittedTextBeforeComposingText.append(text); @@ -285,7 +287,8 @@ public final class RichInputConnection { mTempObjectForCommitText.clear(); mTempObjectForCommitText.append(text); final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(color); - mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, text.length(), + final int spanLength = Math.min(coloredTextLength, text.length()); + mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, spanLength, Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mIC.commitText(mTempObjectForCommitText, newCursorPosition); mLastCommittedTextHasBackgroundColor = true; diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 4ad5ba65e..1ecc995b2 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -188,8 +188,14 @@ public final class Suggest { suggestionsList = suggestionsContainer; } - final int inputStyle = resultsArePredictions ? SuggestedWords.INPUT_STYLE_PREDICTION : - inputStyleIfNotPrediction; + final int inputStyle; + if (resultsArePredictions) { + inputStyle = suggestionResults.mIsBeginningOfSentence + ? SuggestedWords.INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION + : SuggestedWords.INPUT_STYLE_PREDICTION; + } else { + inputStyle = inputStyleIfNotPrediction; + } callback.onGetSuggestedWords(new SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, // TODO: this first argument is lying. If this is a whitelisted word which is an @@ -244,6 +250,8 @@ public final class Suggest { // In the batch input mode, the most relevant suggested word should act as a "typed word" // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false). + // Note that because this method is never used to get predictions, there is no need to + // modify inputType such in getSuggestedWordsForNonBatchInput. callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer, suggestionResults.mRawSuggestions, true /* typedWordValid */, diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index dcfaa3f6d..3eefafc1f 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -39,6 +39,7 @@ public class SuggestedWords { public static final int INPUT_STYLE_APPLICATION_SPECIFIED = 4; public static final int INPUT_STYLE_RECORRECTION = 5; public static final int INPUT_STYLE_PREDICTION = 6; + public static final int INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION = 7; // The maximum number of suggestions available. public static final int MAX_SUGGESTIONS = 18; @@ -80,10 +81,9 @@ public class SuggestedWords { final int inputStyle, final int sequenceNumber) { this(suggestedWordInfoList, rawSuggestions, - (suggestedWordInfoList.isEmpty() || INPUT_STYLE_PREDICTION == inputStyle) ? null + (suggestedWordInfoList.isEmpty() || isPrediction(inputStyle)) ? null : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord, - typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, - sequenceNumber); + typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, sequenceNumber); } public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, @@ -180,6 +180,7 @@ public class SuggestedWords { return "SuggestedWords:" + " mTypedWordValid=" + mTypedWordValid + " mWillAutoCorrect=" + mWillAutoCorrect + + " mInputStyle=" + mInputStyle + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray()); } @@ -386,8 +387,13 @@ public class SuggestedWords { } } + private static boolean isPrediction(final int inputStyle) { + return INPUT_STYLE_PREDICTION == inputStyle + || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle; + } + public boolean isPrediction() { - return INPUT_STYLE_PREDICTION == mInputStyle; + return isPrediction(mInputStyle); } // SuggestedWords is an immutable object, as much as possible. We must not just remove diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 0942c078f..8eccd5cee 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -28,6 +28,7 @@ import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; @@ -91,12 +92,8 @@ public final class InputLogic { private final TextDecorator mTextDecorator = new TextDecorator(new TextDecorator.Listener() { @Override - public void onClickComposingTextToCommit(SuggestedWordInfo wordInfo) { - mLatinIME.pickSuggestionManually(wordInfo); - } - @Override - public void onClickComposingTextToAddToDictionary(SuggestedWordInfo wordInfo) { - mLatinIME.addWordToUserDictionary(wordInfo.mWord); + public void onClickComposingTextToAddToDictionary(final String word) { + mLatinIME.addWordToUserDictionary(word); mLatinIME.dismissAddToDictionaryHint(); } }); @@ -171,6 +168,7 @@ public final class InputLogic { mConnection.requestCursorUpdates(true /* enableMonitor */, true /* requestImmediateCallback */); } + mTextDecorator.reset(); } } @@ -207,6 +205,8 @@ public final class InputLogic { public void finishInput() { if (mWordComposer.isComposingWord()) { mConnection.finishComposingText(); + StatsUtils.onWordCommitUserTyped( + mWordComposer.getTypedWord(), mWordComposer.isBatchMode()); } resetComposingState(true /* alsoResetLastComposedWord */); mInputLogicHandler.reset(); @@ -253,6 +253,7 @@ public final class InputLogic { promotePhantomSpace(settingsValues); } mConnection.commitText(text, 1); + StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode()); mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState mSpaceState = SpaceState.NONE; @@ -334,17 +335,8 @@ public final class InputLogic { } final boolean shouldShowAddToDictionaryHint = shouldShowAddToDictionaryHint(suggestionInfo); - final boolean shouldShowAddToDictionaryIndicator = - shouldShowAddToDictionaryHint && settingsValues.mShouldShowUiToAcceptTypedWord; - final int backgroundColor; - if (shouldShowAddToDictionaryIndicator) { - backgroundColor = settingsValues.mTextHighlightColorForAddToDictionaryIndicator; - } else { - backgroundColor = Color.TRANSPARENT; - } - commitChosenWordWithBackgroundColor(settingsValues, suggestion, - LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR, - backgroundColor); + commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, + LastComposedWord.NOT_A_SEPARATOR); mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); @@ -359,11 +351,10 @@ public final class InputLogic { // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE. handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); } - if (shouldShowAddToDictionaryIndicator) { - mTextDecorator.showAddToDictionaryIndicator(suggestionInfo); - } StatsUtils.onPickSuggestionManually(mSuggestedWords, suggestionInfo); + StatsUtils.onWordCommitSuggestionPickedManually( + suggestionInfo.mWord, mWordComposer.isBatchMode()); return inputTransaction; } @@ -433,6 +424,9 @@ public final class InputLogic { mRecapitalizeStatus.enable(); // We moved the cursor and need to invalidate the indicator right now. mTextDecorator.reset(); + // Remaining background color that was used for the add-to-dictionary indicator should be + // removed. + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); // We moved the cursor. If we are touching a word, we need to resume suggestion. mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */, true /* shouldDelay */); @@ -511,7 +505,9 @@ public final class InputLogic { handler.cancelUpdateSuggestionStrip(); ++mAutoCommitSequenceNumber; mConnection.beginBatchEdit(); - if (mWordComposer.isComposingWord()) { + if (!mWordComposer.isComposingWord()) { + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); + } else { if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the batch input at the current cursor position. @@ -582,6 +578,7 @@ public final class InputLogic { batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); promotePhantomSpace(settingsValues); mConnection.commitText(commitParts[0], 0); + StatsUtils.onWordCommitUserTyped(commitParts[0], mWordComposer.isBatchMode()); mSpaceState = SpaceState.PHANTOM; keyboardSwitcher.requestUpdatingShiftState( getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState()); @@ -630,42 +627,6 @@ public final class InputLogic { } mSuggestedWords = suggestedWords; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; - if (shouldShowCommitIndicator(suggestedWords, settingsValues)) { - // typedWordInfo is never null here. - final int textBackgroundColor = settingsValues.mTextHighlightColorForCommitIndicator; - final SuggestedWordInfo typedWordInfo = suggestedWords.getTypedWordInfoOrNull(); - handler.postShowCommitIndicatorTask(new Runnable() { - @Override - public void run() { - // TODO: This needs to be refactored to ensure that mWordComposer is accessed - // only from the UI thread. - if (!mWordComposer.isComposingWord()) { - mTextDecorator.reset(); - return; - } - final SuggestedWordInfo currentTypedWordInfo = - mSuggestedWords.getTypedWordInfoOrNull(); - if (currentTypedWordInfo == null) { - mTextDecorator.reset(); - return; - } - if (!currentTypedWordInfo.equals(typedWordInfo)) { - // Suggested word has been changed. This task is obsolete. - mTextDecorator.reset(); - return; - } - // TODO: As with the above TODO comment, this operation must be performed only - // on the UI thread too. Needs to be refactored. - setComposingTextInternalWithBackgroundColor(typedWordInfo.mWord, - 1 /* newCursorPosition */, textBackgroundColor); - mTextDecorator.showCommitIndicator(typedWordInfo); - } - }); - } else { - // Note: It is OK to not cancel previous postShowCommitIndicatorTask() here. Having a - // cancellation mechanism could improve performance a bit though. - mTextDecorator.reset(); - } // Put a blue underline to a word in TextView which will be auto-corrected. if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator @@ -843,13 +804,14 @@ public final class InputLogic { final InputTransaction inputTransaction, // TODO: remove this argument final LatinIME.UIHandler handler) { - // In case the "add to dictionary" hint was still displayed. - // TODO: Do we really need to check if we have composing text here? - if (!mWordComposer.isComposingWord() && - mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) { - mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); + if (!mWordComposer.isComposingWord()) { mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); - mTextDecorator.reset(); + // In case the "add to dictionary" hint was still displayed. + // TODO: Do we really need to check if we have composing text here? + if (mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) { + mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); + mTextDecorator.reset(); + } } final int codePoint = event.mCodePoint; @@ -1108,8 +1070,10 @@ public final class InputLogic { inputTransaction.setRequiresUpdateSuggestions(); } else { if (mLastComposedWord.canRevertCommit()) { - revertCommit(inputTransaction); + final String lastComposedWord = mLastComposedWord.mTypedWord; + revertCommit(inputTransaction, inputTransaction.mSettingsValues); StatsUtils.onRevertAutoCorrect(); + StatsUtils.onWordCommitUserTyped(lastComposedWord, mWordComposer.isBatchMode()); return; } if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { @@ -1609,14 +1573,19 @@ public final class InputLogic { * This is triggered upon pressing backspace just after a commit with auto-correction. * * @param inputTransaction The transaction in progress. + * @param settingsValues the current values of the settings. */ - private void revertCommit(final InputTransaction inputTransaction) { + private void revertCommit(final InputTransaction inputTransaction, + final SettingsValues settingsValues) { final CharSequence originallyTypedWord = mLastComposedWord.mTypedWord; + final String originallyTypedWordString = + originallyTypedWord != null ? originallyTypedWord.toString() : ""; final CharSequence committedWord = mLastComposedWord.mCommittedWord; final String committedWordString = committedWord.toString(); final int cancelLength = committedWord.length(); + final String separatorString = mLastComposedWord.mSeparatorString; // We want java chars, not codepoints for the following. - final int separatorLength = mLastComposedWord.mSeparatorString.length(); + final int separatorLength = separatorString.length(); // TODO: should we check our saved separator against the actual contents of the text view? final int deleteLength = cancelLength + separatorLength; if (DebugFlags.DEBUG_ENABLED) { @@ -1635,7 +1604,7 @@ public final class InputLogic { if (!TextUtils.isEmpty(committedWord)) { mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString); } - final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; + final String stringToCommit = originallyTypedWord + separatorString; final SpannableString textToCommit = new SpannableString(stringToCommit); if (committedWord instanceof SpannableString) { final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord; @@ -1672,23 +1641,53 @@ public final class InputLogic { suggestions.toArray(new String[suggestions.size()]), 0 /* flags */), 0 /* start */, lastCharIndex /* end */, 0 /* flags */); } + + final boolean shouldShowAddToDictionaryForTypedWord = + shouldShowAddToDictionaryForTypedWord(mLastComposedWord, settingsValues); + if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { // For languages with spaces, we revert to the typed string, but the cursor is still // after the separator so we don't resume suggestions. If the user wants to correct // the word, they have to press backspace again. - mConnection.commitText(textToCommit, 1); + if (shouldShowAddToDictionaryForTypedWord) { + mConnection.commitTextWithBackgroundColor(textToCommit, 1, + settingsValues.mTextHighlightColorForAddToDictionaryIndicator, + originallyTypedWordString.length()); + } else { + mConnection.commitText(textToCommit, 1); + } } else { // For languages without spaces, we revert the typed string but the cursor is flush // with the typed word, so we need to resume suggestions right away. final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); mWordComposer.setComposingWord(codePoints, mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); - setComposingTextInternal(textToCommit, 1); + if (shouldShowAddToDictionaryForTypedWord) { + setComposingTextInternalWithBackgroundColor(textToCommit, 1, + settingsValues.mTextHighlightColorForAddToDictionaryIndicator, + originallyTypedWordString.length()); + } else { + setComposingTextInternal(textToCommit, 1); + } } // Don't restart suggestion yet. We'll restart if the user deletes the separator. mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - // We have a separator between the word and the cursor: we should show predictions. - inputTransaction.setRequiresUpdateSuggestions(); + + if (shouldShowAddToDictionaryForTypedWord) { + // Due to the API limitation as of L, we cannot reliably retrieve the reverted text + // when the separator causes line breaking. Until this API limitation is addressed in + // the framework, show the indicator only when the separator doesn't contain + // line-breaking characters. + if (!StringUtils.hasLineBreakCharacter(separatorString)) { + mTextDecorator.showAddToDictionaryIndicator(originallyTypedWordString, + mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd()); + } + mSuggestionStripViewAccessor.showAddToDictionaryHint(originallyTypedWordString); + } else { + // We have a separator between the word and the cursor: we should show predictions. + inputTransaction.setRequiresUpdateSuggestions(); + } } /** @@ -2010,6 +2009,8 @@ public final class InputLogic { final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; if (0 != indexOfLastSpace) { mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); + StatsUtils.onWordCommitUserTyped( + batchInputText.substring(0, indexOfLastSpace), mWordComposer.isBatchMode()); final SuggestedWords suggestedWordsForLastWordOfPhraseGesture = suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture(); mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture); @@ -2048,8 +2049,10 @@ public final class InputLogic { if (!mWordComposer.isComposingWord()) return; final String typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { + final boolean isBatchMode = mWordComposer.isBatchMode(); commitChosenWord(settingsValues, typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString); + StatsUtils.onWordCommitUserTyped(typedWord, isBatchMode); } } @@ -2095,6 +2098,7 @@ public final class InputLogic { throw new RuntimeException("We have an auto-correction but the typed word " + "is empty? Impossible! I must commit suicide."); } + final boolean isBatchMode = mWordComposer.isBatchMode(); commitChosenWord(settingsValues, autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator); if (!typedWord.equals(autoCorrection)) { @@ -2107,16 +2111,17 @@ public final class InputLogic { mConnection.commitCorrection(new CorrectionInfo( mConnection.getExpectedSelectionEnd() - autoCorrection.length(), typedWord, autoCorrection)); - StatsUtils.onAutoCorrection(typedWord, autoCorrection, mWordComposer.isBatchMode(), + StatsUtils.onAutoCorrection(typedWord, autoCorrection, isBatchMode, mWordComposer.getAutoCorrectionDictionaryTypeOrNull()); + StatsUtils.onWordCommitAutoCorrect(autoCorrection, isBatchMode); + } else { + StatsUtils.onWordCommitUserTyped(autoCorrection, isBatchMode); } } } /** - * Commits the chosen word to the text field and saves it for later retrieval. This is a - * synonym of {@code commitChosenWordWithBackgroundColor(settingsValues, chosenWord, - * commitType, separatorString, Color.TRANSPARENT}. + * Commits the chosen word to the text field and saves it for later retrieval. * * @param settingsValues the current values of the settings. * @param chosenWord the word we want to commit. @@ -2125,23 +2130,6 @@ public final class InputLogic { */ private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord, final int commitType, final String separatorString) { - commitChosenWordWithBackgroundColor(settingsValues, chosenWord, commitType, separatorString, - Color.TRANSPARENT); - } - - /** - * Commits the chosen word to the text field and saves it for later retrieval. - * - * @param settingsValues the current values of the settings. - * @param chosenWord the word we want to commit. - * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_* - * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. - * @param backgroundColor the background color to be specified with the committed text. Pass - * {@link Color#TRANSPARENT} to not specify the background color. - */ - private void commitChosenWordWithBackgroundColor(final SettingsValues settingsValues, - final String chosenWord, final int commitType, final String separatorString, - final int backgroundColor) { final SuggestedWords suggestedWords = mSuggestedWords; final CharSequence chosenWordWithSuggestions = SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, @@ -2151,7 +2139,7 @@ public final class InputLogic { // information from the 1st previous word. final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord( settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1); - mConnection.commitTextWithBackgroundColor(chosenWordWithSuggestions, 1, backgroundColor); + mConnection.commitText(chosenWordWithSuggestions, 1); // Add the word to the user history dictionary performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWordsInfo); // TODO: figure out here if this is an auto-correct or if the best word is actually @@ -2235,7 +2223,7 @@ public final class InputLogic { private void setComposingTextInternal(final CharSequence newComposingText, final int newCursorPosition) { setComposingTextInternalWithBackgroundColor(newComposingText, newCursorPosition, - Color.TRANSPARENT); + Color.TRANSPARENT, newComposingText.length()); } /** @@ -2251,9 +2239,11 @@ public final class InputLogic { * @param newCursorPosition the new cursor position * @param backgroundColor the background color to be set to the composing text. Set * {@link Color#TRANSPARENT} to disable the background color. + * @param coloredTextLength the length of text, in Java chars, which should be rendered with + * the given background color. */ private void setComposingTextInternalWithBackgroundColor(final CharSequence newComposingText, - final int newCursorPosition, final int backgroundColor) { + final int newCursorPosition, final int backgroundColor, final int coloredTextLength) { final CharSequence composingTextToBeSet; if (backgroundColor == Color.TRANSPARENT) { composingTextToBeSet = newComposingText; @@ -2261,7 +2251,8 @@ public final class InputLogic { final SpannableString spannable = new SpannableString(newComposingText); final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(backgroundColor); - spannable.setSpan(backgroundColorSpan, 0, spannable.length(), + final int spanLength = Math.min(coloredTextLength, spannable.length()); + spannable.setSpan(backgroundColorSpan, 0, spanLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); composingTextToBeSet = spannable; } @@ -2283,7 +2274,8 @@ public final class InputLogic { } /** - * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo} is called. + * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is + * called. * @param info The wrapper object with which we can access cursor/anchor info. */ public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { @@ -2307,12 +2299,12 @@ public final class InputLogic { } /** - * Returns whether the commit indicator should be shown or not. - * @param suggestedWords the suggested word that is being displayed. + * Returns whether the add to dictionary indicator should be shown or not. + * @param lastComposedWord the last composed word information. * @param settingsValues the current settings value. * @return {@code true} if the commit indicator should be shown. */ - private boolean shouldShowCommitIndicator(final SuggestedWords suggestedWords, + private boolean shouldShowAddToDictionaryForTypedWord(final LastComposedWord lastComposedWord, final SettingsValues settingsValues) { if (!mConnection.isCursorAnchorInfoMonitorEnabled()) { // We cannot help in this case because we are heavily relying on this new API. @@ -2321,24 +2313,16 @@ public final class InputLogic { if (!settingsValues.mShouldShowUiToAcceptTypedWord) { return false; } - final SuggestedWordInfo typedWordInfo = suggestedWords.getTypedWordInfoOrNull(); - if (typedWordInfo == null) { + if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) { return false; } - if (suggestedWords.mInputStyle != SuggestedWords.INPUT_STYLE_TYPING){ + if (TextUtils.equals(lastComposedWord.mTypedWord, lastComposedWord.mCommittedWord)) { return false; } - if (settingsValues.mShowCommitIndicatorOnlyForAutoCorrection - && !suggestedWords.mWillAutoCorrect) { + if (!mDictionaryFacilitator.isUserDictionaryEnabled()) { return false; } - // TODO: Calling shouldShowAddToDictionaryHint(typedWordInfo) multiple times should be fine - // in terms of performance, but we can do better. One idea is to make SuggestedWords include - // a boolean that tells whether the word is a dictionary word or not. - if (settingsValues.mShowCommitIndicatorOnlyForOutOfVocabulary - && !shouldShowAddToDictionaryHint(typedWordInfo)) { - return false; - } - return true; + return !mDictionaryFacilitator.isValidWord(lastComposedWord.mTypedWord, + true /* ignoreCase */); } } diff --git a/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java new file mode 100644 index 000000000..06ab1e2d2 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.settings; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.text.TextUtils; +import android.widget.ListView; +import android.widget.Toast; + +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SubtypeSwitcher; +import com.android.inputmethod.latin.define.ProductionFlags; +import com.android.inputmethod.latin.utils.LoginAccountUtils; + +import javax.annotation.Nullable; + +/** + * "Accounts & Privacy" settings sub screen. + * + * This settings sub screen handles the following preferences: + * <li> Account selection/management for IME + * <li> TODO: Sync preferences + * <li> TODO: Privacy preferences + */ +public final class AccountsSettingsFragment extends SubScreenFragment { + static final String PREF_ACCCOUNT_SWITCHER = "account_switcher"; + + private final DialogInterface.OnClickListener mAccountSelectedListener = + new AccountSelectedListener(); + private final DialogInterface.OnClickListener mAccountSignedOutListener = + new AccountSignedOutListener(); + + @Override + public void onCreate(final Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.prefs_screen_accounts); + + final Resources res = getResources(); + final Context context = getActivity(); + + // When we are called from the Settings application but we are not already running, some + // singleton and utility classes may not have been initialized. We have to call + // initialization method of these classes here. See {@link LatinIME#onCreate()}. + SubtypeSwitcher.init(context); + + if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) { + final Preference enableMetricsLogging = + findPreference(Settings.PREF_ENABLE_METRICS_LOGGING); + if (enableMetricsLogging != null) { + final String enableMetricsLoggingTitle = res.getString( + R.string.enable_metrics_logging, getApplicationName()); + enableMetricsLogging.setTitle(enableMetricsLoggingTitle); + } + } else { + removePreference(Settings.PREF_ENABLE_METRICS_LOGGING); + } + } + + @Override + public void onResume() { + super.onResume(); + refreshAccountSelection(); + } + + @Override + public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { + // TODO: Look at the preference that changed before refreshing the view. + refreshAccountSelection(); + } + + private void refreshAccountSelection() { + final String currentAccount = getCurrentlySelectedAccount(); + final Preference accountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER); + if (currentAccount == null) { + // No account is currently selected. + accountSwitcher.setSummary(getString(R.string.no_accounts_selected)); + } else { + // Set the currently selected account. + accountSwitcher.setSummary(getString(R.string.account_selected, currentAccount)); + } + final Context context = getActivity(); + final String[] accountsForLogin = LoginAccountUtils.getAccountsForLogin(context); + accountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + if (accountsForLogin.length == 0) { + // TODO: Handle account addition. + Toast.makeText(getActivity(), + getString(R.string.account_select_cancel), Toast.LENGTH_SHORT).show(); + } else { + createAccountPicker(accountsForLogin, currentAccount).show(); + } + return true; + } + }); + + // TODO: Depending on the account selection, enable/disable preferences that + // depend on an account. + } + + @Nullable + private String getCurrentlySelectedAccount() { + return getSharedPreferences().getString(Settings.PREF_ACCOUNT_NAME, null); + } + + /** + * Creates an account picker dialog showing the given accounts in a list and selecting + * the selected account by default. + * The list of accounts must not be null/empty. + * + * Package-private for testing. + */ + AlertDialog createAccountPicker(final String[] accounts, + final String selectedAccount) { + if (accounts == null || accounts.length == 0) { + throw new IllegalArgumentException("List of accounts must not be empty"); + } + + // See if the currently selected account is in the list. + // If it is, the entry is selected, and a sign-out button is provided. + // If it isn't, select the 0th account by default which will get picked up + // if the user presses OK. + int index = 0; + boolean isSignedIn = false; + for (int i = 0; i < accounts.length; i++) { + if (TextUtils.equals(accounts[i], selectedAccount)) { + index = i; + isSignedIn = true; + break; + } + } + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) + .setTitle(R.string.account_select_title) + .setSingleChoiceItems(accounts, index, null) + .setPositiveButton(R.string.account_select_ok, mAccountSelectedListener) + .setNegativeButton(R.string.account_select_cancel, null); + if (isSignedIn) { + builder.setNeutralButton(R.string.account_select_sign_out, mAccountSignedOutListener); + } + return builder.create(); + } + + /** + * Listener for an account being selected from the picker. + * Persists the account to shared preferences. + */ + class AccountSelectedListener implements DialogInterface.OnClickListener { + @Override + public void onClick(DialogInterface dialog, int which) { + final ListView lv = ((AlertDialog)dialog).getListView(); + final Object selectedItem = lv.getItemAtPosition(lv.getCheckedItemPosition()); + getSharedPreferences() + .edit() + .putString(Settings.PREF_ACCOUNT_NAME, (String) selectedItem) + .apply(); + } + } + + /** + * Listener for sign-out being initiated from from the picker. + * Removed the account from shared preferences. + */ + class AccountSignedOutListener implements DialogInterface.OnClickListener { + @Override + public void onClick(DialogInterface dialog, int which) { + getSharedPreferences() + .edit() + .remove(Settings.PREF_ACCOUNT_NAME) + .apply(); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java index 00f2c73dd..a6cb55db1 100644 --- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java @@ -93,14 +93,16 @@ public final class AdvancedSettingsFragment extends SubScreenFragment { removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON); } + // If metrics logging isn't supported, or account sign in is enabled + // don't show the logging preference. + // TODO: Eventually when we enable account sign in by default, + // we'll remove logging preference from here. if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) { final Preference enableMetricsLogging = findPreference(Settings.PREF_ENABLE_METRICS_LOGGING); if (enableMetricsLogging != null) { - final int applicationLabelRes = context.getApplicationInfo().labelRes; - final String applicationName = res.getString(applicationLabelRes); final String enableMetricsLoggingTitle = res.getString( - R.string.enable_metrics_logging, applicationName); + R.string.enable_metrics_logging, getApplicationName()); enableMetricsLogging.setTitle(enableMetricsLoggingTitle); } } else { diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 529f8a093..a171fc330 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -43,6 +43,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang private static final String TAG = Settings.class.getSimpleName(); // Settings screens public static final String SCREEN_PREFERENCES = "screen_preferences"; + public static final String SCREEN_ACCOUNTS = "screen_accounts"; public static final String SCREEN_APPEARANCE = "screen_appearance"; public static final String SCREEN_THEME = "screen_theme"; public static final String SCREEN_MULTILINGUAL = "screen_multilingual"; @@ -104,6 +105,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal"; public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging"; + public static final String PREF_ACCOUNT_NAME = "pref_account_name"; // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead. // This is being used only for the backward compatibility. diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java index 4fc17387f..8c4801798 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java @@ -25,6 +25,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.define.ProductionFlags; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.FeedbackUtils; import com.android.inputmethodcommon.InputMethodSettingsFragment; @@ -51,6 +52,10 @@ public final class SettingsFragment extends InputMethodSettingsFragment { final Preference multilingualOptions = findPreference(Settings.SCREEN_MULTILINGUAL); preferenceScreen.removePreference(multilingualOptions); } + if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { + final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS); + preferenceScreen.removePreference(accountsPreference); + } } @Override diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index 57610221c..3339ab57f 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -102,10 +102,7 @@ public class SettingsValues { new int[AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE]; // TextDecorator - public final int mTextHighlightColorForCommitIndicator; public final int mTextHighlightColorForAddToDictionaryIndicator; - public final boolean mShowCommitIndicatorOnlyForAutoCorrection; - public final boolean mShowCommitIndicatorOnlyForOutOfVocabulary; // Debug settings public final boolean mIsInternal; @@ -183,12 +180,6 @@ public class SettingsValues { mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs); AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray( prefs, mAdditionalFeaturesSettingValues); - mShowCommitIndicatorOnlyForAutoCorrection = res.getBoolean( - R.bool.text_decorator_only_for_auto_correction); - mShowCommitIndicatorOnlyForOutOfVocabulary = res.getBoolean( - R.bool.text_decorator_only_for_out_of_vocabulary); - mTextHighlightColorForCommitIndicator = res.getColor( - R.color.text_decorator_commit_indicator_text_highlight_color); mTextHighlightColorForAddToDictionaryIndicator = res.getColor( R.color.text_decorator_add_to_dictionary_indicator_text_highlight_color); mIsInternal = Settings.isInternal(prefs); @@ -443,12 +434,6 @@ public class SettingsValues { sb.append("" + (null == awu ? "null" : awu.toString())); sb.append("\n mAdditionalFeaturesSettingValues = "); sb.append("" + Arrays.toString(mAdditionalFeaturesSettingValues)); - sb.append("\n mShowCommitIndicatorOnlyForAutoCorrection = "); - sb.append("" + mShowCommitIndicatorOnlyForAutoCorrection); - sb.append("\n mShowCommitIndicatorOnlyForOutOfVocabulary = "); - sb.append("" + mShowCommitIndicatorOnlyForOutOfVocabulary); - sb.append("\n mTextHighlightColorForCommitIndicator = "); - sb.append("" + mTextHighlightColorForCommitIndicator); sb.append("\n mTextHighlightColorForAddToDictionaryIndicator = "); sb.append("" + mTextHighlightColorForAddToDictionaryIndicator); sb.append("\n mIsInternal = "); diff --git a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java index ca5b395ce..240f8f89b 100644 --- a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java @@ -20,6 +20,7 @@ import android.app.backup.BackupManager; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.content.res.Resources; import android.os.Bundle; import android.preference.ListPreference; import android.preference.Preference; @@ -79,6 +80,16 @@ abstract class SubScreenFragment extends PreferenceFragment return getPreferenceManager().getSharedPreferences(); } + /** + * Gets the application name to display on the UI. + */ + final String getApplicationName() { + final Context context = getActivity(); + final Resources res = getResources(); + final int applicationLabelRes = context.getApplicationInfo().labelRes; + return res.getString(applicationLabelRes); + } + @Override public void addPreferencesFromResource(final int preferencesResId) { super.addPreferencesFromResource(preferencesResId); diff --git a/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java new file mode 100644 index 000000000..254bc6567 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.settings; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Intent; +import android.os.Bundle; + +/** + * Test activity to use when testing preference fragments. <br/> + * Usage: <br/> + * Create an ActivityInstrumentationTestCase2 for this activity + * and call setIntent() with an intent that specifies the fragment to load in the activity. + * The fragment can then be obtained from this activity and used for testing/verification. + */ +public final class TestFragmentActivity extends Activity { + /** + * The fragment name that should be loaded when starting this activity. + * This must be specified when starting this activity, as this activity is only + * meant to test fragments from instrumentation tests. + */ + public static final String EXTRA_SHOW_FRAGMENT = "show_fragment"; + + public Fragment mFragment; + + @Override + protected void onCreate(final Bundle savedState) { + super.onCreate(savedState); + final Intent intent = getIntent(); + final String fragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT); + if (fragmentName == null) { + throw new IllegalArgumentException("No fragment name specified for testing"); + } + + mFragment = Fragment.instantiate(this, fragmentName); + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.beginTransaction().add(mFragment, fragmentName).commit(); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java index c2167a76b..ae2de44c7 100644 --- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.dictionarypack.DictionarySettingsFragment; import com.android.inputmethod.latin.about.AboutPreferences; +import com.android.inputmethod.latin.settings.AccountsSettingsFragment; import com.android.inputmethod.latin.settings.AdvancedSettingsFragment; import com.android.inputmethod.latin.settings.AppearanceSettingsFragment; import com.android.inputmethod.latin.settings.CorrectionSettingsFragment; @@ -42,6 +43,7 @@ public class FragmentUtils { sLatinImeFragments.add(DictionarySettingsFragment.class.getName()); sLatinImeFragments.add(AboutPreferences.class.getName()); sLatinImeFragments.add(PreferencesSettingsFragment.class.getName()); + sLatinImeFragments.add(AccountsSettingsFragment.class.getName()); sLatinImeFragments.add(AppearanceSettingsFragment.class.getName()); sLatinImeFragments.add(ThemeSettingsFragment.class.getName()); sLatinImeFragments.add(MultiLingualSettingsFragment.class.getName()); diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index 55557de9d..bbcef990d 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -37,6 +37,14 @@ public final class StringUtils { private static final String EMPTY_STRING = ""; + private static final char CHAR_LINE_FEED = 0X000A; + private static final char CHAR_VERTICAL_TAB = 0X000B; + private static final char CHAR_FORM_FEED = 0X000C; + private static final char CHAR_CARRIAGE_RETURN = 0X000D; + private static final char CHAR_NEXT_LINE = 0X0085; + private static final char CHAR_LINE_SEPARATOR = 0X2028; + private static final char CHAR_PARAGRAPH_SEPARATOR = 0X2029; + private StringUtils() { // This utility class is not publicly instantiable. } @@ -594,4 +602,30 @@ public final class StringUtils { return sb + "]"; } } + + /** + * Returns whether the last composed word contains line-breaking character (e.g. CR or LF). + * @param text the text to be examined. + * @return {@code true} if the last composed word contains line-breaking separator. + */ + @UsedForTesting + public static boolean hasLineBreakCharacter(final String text) { + if (TextUtils.isEmpty(text)) { + return false; + } + for (int i = text.length() - 1; i >= 0; --i) { + final char c = text.charAt(i); + switch (c) { + case CHAR_LINE_FEED: + case CHAR_VERTICAL_TAB: + case CHAR_FORM_FEED: + case CHAR_CARRIAGE_RETURN: + case CHAR_NEXT_LINE: + case CHAR_LINE_SEPARATOR: + case CHAR_PARAGRAPH_SEPARATOR: + return true; + } + } + return false; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java index eaa5743d4..d6f644228 100644 --- a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java +++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java @@ -22,7 +22,6 @@ import com.android.inputmethod.latin.define.ProductionFlags; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; -import java.util.Locale; import java.util.TreeSet; /** @@ -31,14 +30,17 @@ import java.util.TreeSet; */ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { public final ArrayList<SuggestedWordInfo> mRawSuggestions; + // TODO: Instead of a boolean , we may want to include the context of this suggestion results, + // such as {@link PrevWordsInfo}. + public final boolean mIsBeginningOfSentence; private final int mCapacity; - public SuggestionResults(final int capacity) { - this(sSuggestedWordInfoComparator, capacity); + public SuggestionResults(final int capacity, final boolean isBeginningOfSentence) { + this(sSuggestedWordInfoComparator, capacity, isBeginningOfSentence); } - public SuggestionResults(final Comparator<SuggestedWordInfo> comparator, - final int capacity) { + private SuggestionResults(final Comparator<SuggestedWordInfo> comparator, + final int capacity, final boolean isBeginningOfSentence) { super(comparator); mCapacity = capacity; if (ProductionFlags.INCLUDE_RAW_SUGGESTIONS) { @@ -46,6 +48,7 @@ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { } else { mRawSuggestions = null; } + mIsBeginningOfSentence = isBeginningOfSentence; } @Override |