diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
29 files changed, 1141 insertions, 380 deletions
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index 36a02669d..fd1f51dd6 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -491,8 +491,9 @@ public class DictionaryFacilitator { final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { final Dictionaries dictionaries = mDictionaries; - final SuggestionResults suggestionResults = - new SuggestionResults(dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS); + final SuggestionResults suggestionResults = new SuggestionResults( + dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS, + prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence); final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT }; for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { final Dictionary dictionary = dictionaries.getDict(dictType); diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java index ebe436128..fecb0ef94 100644 --- a/java/src/com/android/inputmethod/latin/InputAttributes.java +++ b/java/src/com/android/inputmethod/latin/InputAttributes.java @@ -42,6 +42,7 @@ public final class InputAttributes { final public boolean mApplicationSpecifiedCompletionOn; final public boolean mShouldInsertSpacesAutomatically; final public boolean mShouldShowVoiceInputKey; + final public boolean mIsGeneralTextInput; final private int mInputType; final private EditorInfo mEditorInfo; final private String mPackageNameForPrivateImeOptions; @@ -76,6 +77,7 @@ public final class InputAttributes { mApplicationSpecifiedCompletionOn = false; mShouldInsertSpacesAutomatically = false; mShouldShowVoiceInputKey = false; + mIsGeneralTextInput = false; return; } // inputClass == InputType.TYPE_CLASS_TEXT @@ -102,7 +104,7 @@ public final class InputAttributes { mShouldInsertSpacesAutomatically = InputTypeUtils.isAutoSpaceFriendlyType(inputType); final boolean noMicrophone = mIsPasswordField - || InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS == variation + || InputTypeUtils.isEmailVariation(variation) || InputType.TYPE_TEXT_VARIATION_URI == variation || hasNoMicrophoneKeyOption(); mShouldShowVoiceInputKey = !noMicrophone; @@ -117,6 +119,15 @@ public final class InputAttributes { || (!flagAutoCorrect && !flagMultiLine); mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode; + + // If we come here, inputClass is always TYPE_CLASS_TEXT + mIsGeneralTextInput = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS != variation + && InputType.TYPE_TEXT_VARIATION_PASSWORD != variation + && InputType.TYPE_TEXT_VARIATION_PHONETIC != variation + && InputType.TYPE_TEXT_VARIATION_URI != variation + && InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD != variation + && InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != variation + && InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD != variation; } public boolean isTypeNull() { diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java index e9e12f09f..7fa935413 100644 --- a/java/src/com/android/inputmethod/latin/InputView.java +++ b/java/src/com/android/inputmethod/latin/InputView.java @@ -21,14 +21,14 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.widget.LinearLayout; +import android.widget.FrameLayout; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.suggestions.MoreSuggestionsView; import com.android.inputmethod.latin.suggestions.SuggestionStripView; -public final class InputView extends LinearLayout { +public final class InputView extends FrameLayout { private final Rect mInputViewRect = new Rect(); private MainKeyboardView mMainKeyboardView; private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder; diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 6045b3428..d57db8e9a 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -29,7 +29,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Rect; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.ConnectivityManager; @@ -43,21 +42,22 @@ import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.SparseArray; +import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodSubtype; +import android.widget.TextView; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; -import com.android.inputmethod.compat.InputConnectionCompatUtils; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.event.Event; @@ -69,6 +69,7 @@ import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.keyboard.TextDecoratorUi; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.DebugFlags; @@ -86,6 +87,7 @@ import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; +import com.android.inputmethod.latin.utils.CursorAnchorInfoUtils; import com.android.inputmethod.latin.utils.DialogUtils; import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; import com.android.inputmethod.latin.utils.ImportantNoticeUtils; @@ -94,6 +96,7 @@ import com.android.inputmethod.latin.utils.JniUtils; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; +import com.android.inputmethod.latin.utils.ViewLayoutUtils; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -150,15 +153,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: Move these {@link View}s to {@link KeyboardSwitcher}. private View mInputView; - private View mExtractArea; - private View mKeyPreviewBackingView; private SuggestionStripView mSuggestionStripView; + private TextView mExtractEditText; private RichInputMethodManager mRichImm; @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeSwitcher mSubtypeSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); private final SpecialKeyDetector mSpecialKeyDetector; + // Working variable for {@link #startShowingInputView()} and + // {@link #onEvaluateInputViewShown()}. + private boolean mIsExecutingStartShowingInputView; // Object for reacting to adding/removing a dictionary pack. private final BroadcastReceiver mDictionaryPackInstallReceiver = @@ -206,10 +211,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } final Resources res = latinIme.getResources(); - mDelayInMillisecondsToUpdateSuggestions = - res.getInteger(R.integer.config_delay_in_milliseconds_to_update_suggestions); - mDelayInMillisecondsToUpdateShiftState = - res.getInteger(R.integer.config_delay_in_milliseconds_to_update_shift_state); + mDelayInMillisecondsToUpdateSuggestions = res.getInteger( + R.integer.config_delay_in_milliseconds_to_update_suggestions); + mDelayInMillisecondsToUpdateShiftState = res.getInteger( + R.integer.config_delay_in_milliseconds_to_update_shift_state); } @Override @@ -258,7 +263,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen case MSG_RESET_CACHES: final SettingsValues settingsValues = latinIme.mSettings.getCurrent(); if (latinIme.mInputLogic.retryResetCachesAndReturnSuccess( - msg.arg1 == 1 /* tryResumeSuggestions */, + msg.arg1 == ARG1_TRUE /* tryResumeSuggestions */, msg.arg2 /* remainingTries */, this /* handler */)) { // If we were able to reset the caches, then we can reload the keyboard. // Otherwise, we'll do it when we can. @@ -689,11 +694,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onConfigurationChanged(final Configuration conf) { - final SettingsValues settingsValues = mSettings.getCurrent(); + SettingsValues settingsValues = mSettings.getCurrent(); if (settingsValues.mDisplayOrientation != conf.orientation) { mHandler.startOrientationChanging(); mInputLogic.onOrientationChange(mSettings.getCurrent()); } + if (settingsValues.mHasHardwareKeyboard != Settings.readHasHardwareKeyboard(conf)) { + // If the state of having a hardware keyboard changed, then we want to reload the + // settings to adjust for that. + // TODO: we should probably do this unconditionally here, rather than only when we + // have a change in hardware keyboard configuration. + loadSettings(); + settingsValues = mSettings.getCurrent(); + if (settingsValues.mHasHardwareKeyboard) { + // We call cleanupInternalStateForFinishInput() because it's the right thing to do; + // however, it seems at the moment the framework is passing us a seemingly valid + // but actually non-functional InputConnection object. So if this bug ever gets + // fixed we'll be able to remove the composition, but until it is this code is + // actually not doing much. + cleanupInternalStateForFinishInput(); + } + } // TODO: Remove this test. if (!conf.locale.equals(mPersonalizationDictionaryUpdater.getLocale())) { refreshPersonalizationDictionarySession(settingsValues); @@ -710,13 +731,54 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void setInputView(final View view) { super.setInputView(view); mInputView = view; - mExtractArea = getWindow().getWindow().getDecorView() - .findViewById(android.R.id.extractArea); - mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); if (hasSuggestionStripView()) { mSuggestionStripView.setListener(this, view); } + mInputLogic.setTextDecoratorUi(new TextDecoratorUi(this, view)); + } + + @Override + public void setExtractView(final View view) { + final TextView prevExtractEditText = mExtractEditText; + super.setExtractView(view); + TextView nextExtractEditText = null; + if (view != null) { + final View extractEditText = view.findViewById(android.R.id.inputExtractEditText); + if (extractEditText instanceof TextView) { + nextExtractEditText = (TextView)extractEditText; + } + } + if (prevExtractEditText == nextExtractEditText) { + return; + } + if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && prevExtractEditText != null) { + prevExtractEditText.getViewTreeObserver().removeOnPreDrawListener( + mExtractTextViewPreDrawListener); + } + mExtractEditText = nextExtractEditText; + if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && mExtractEditText != null) { + mExtractEditText.getViewTreeObserver().addOnPreDrawListener( + mExtractTextViewPreDrawListener); + } + } + + private final ViewTreeObserver.OnPreDrawListener mExtractTextViewPreDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + onExtractTextViewPreDraw(); + return true; + } + }; + + private void onExtractTextViewPreDraw() { + if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || !isFullscreenMode() + || mExtractEditText == null) { + return; + } + final CursorAnchorInfo info = CursorAnchorInfoUtils.getCursorAnchorInfo(mExtractEditText); + mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); } @Override @@ -750,20 +812,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() // is not guaranteed. It may even be called at the same time on a different thread. mSubtypeSwitcher.onSubtypeChanged(subtype); - mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype)); + mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype), + mSettings.getCurrent()); loadKeyboard(); } private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInput(editorInfo, restarting); - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { - // AcceptTypedWord feature relies on CursorAnchorInfo. - if (mSettings.getCurrent().mShouldShowUiToAcceptTypedWord) { - InputConnectionCompatUtils.requestUpdateCursorAnchorInfo( - getCurrentInputConnection(), true /* enableMonitor */, - true /* requestImmediateCallback */); - } - } } @SuppressWarnings("deprecation") @@ -828,39 +883,52 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Note: This call should be done by InputMethodService? updateFullscreenMode(); - // The app calling setText() has the effect of clearing the composing - // span, so we should reset our state unconditionally, even if restarting is true. - // We also tell the input logic about the combining rules for the current subtype, so - // it can adjust its combiners if needed. - mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype()); + // ALERT: settings have not been reloaded and there is a chance they may be stale. + // In the practice, if it is, we should have gotten onConfigurationChanged so it should + // be fine, but this is horribly confusing and must be fixed AS SOON AS POSSIBLE. - // Note: the following does a round-trip IPC on the main thread: be careful - final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); + // In some cases the input connection has not been reset yet and we can't access it. In + // this case we will need to call loadKeyboard() later, when it's accessible, so that we + // can go into the correct mode, so we need to do some housekeeping here. + final boolean needToCallLoadKeyboardLater; final Suggest suggest = mInputLogic.mSuggest; - if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) { - // TODO: Do this automatically. - resetSuggest(); - } + if (!currentSettingsValues.mHasHardwareKeyboard) { + // The app calling setText() has the effect of clearing the composing + // span, so we should reset our state unconditionally, even if restarting is true. + // We also tell the input logic about the combining rules for the current subtype, so + // it can adjust its combiners if needed. + mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype(), + currentSettingsValues); + + // Note: the following does a round-trip IPC on the main thread: be careful + final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); + if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) { + // TODO: Do this automatically. + resetSuggest(); + } - // TODO[IL]: Can the following be moved to InputLogic#startInput? - final boolean canReachInputConnection; - if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( - editorInfo.initialSelStart, editorInfo.initialSelEnd, - false /* shouldFinishComposition */)) { - // Sometimes, while rotating, for some reason the framework tells the app we are not - // connected to it and that means we can't refresh the cache. In this case, schedule a - // refresh later. - // We try resetting the caches up to 5 times before giving up. - mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); - // mLastSelection{Start,End} are reset later in this method, don't need to do it here - canReachInputConnection = false; + // TODO[IL]: Can the following be moved to InputLogic#startInput? + if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( + editorInfo.initialSelStart, editorInfo.initialSelEnd, + false /* shouldFinishComposition */)) { + // Sometimes, while rotating, for some reason the framework tells the app we are not + // connected to it and that means we can't refresh the cache. In this case, schedule + // a refresh later. + // We try resetting the caches up to 5 times before giving up. + mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); + // mLastSelection{Start,End} are reset later in this method, no need to do it here + needToCallLoadKeyboardLater = true; + } else { + // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best + // effort to work around this bug. + mInputLogic.mConnection.tryFixLyingCursorPosition(); + mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, + true /* shouldDelay */); + needToCallLoadKeyboardLater = false; + } } else { - // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best - // effort to work around this bug. - mInputLogic.mConnection.tryFixLyingCursorPosition(); - mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, - true /* shouldDelay */); - canReachInputConnection = true; + // If we have a hardware keyboard we don't need to call loadKeyboard later anyway. + needToCallLoadKeyboardLater = false; } if (isDifferentTextField || @@ -878,9 +946,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); - if (!canReachInputConnection) { - // If we can't reach the input connection, we will call loadKeyboard again later, - // so we need to save its state now. The call will be done in #retryResetCaches. + if (needToCallLoadKeyboardLater) { + // If we need to call loadKeyboard again later, we need to save its state now. The + // later call will be done in #retryResetCaches. switcher.saveKeyboardState(); } } else if (restarting) { @@ -937,6 +1005,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void onFinishInputViewInternal(final boolean finishingInput) { super.onFinishInputView(finishingInput); + cleanupInternalStateForFinishInput(); + } + + private void cleanupInternalStateForFinishInput() { mKeyboardSwitcher.deallocateMemory(); // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); @@ -956,13 +1028,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } - // If the keyboard is not visible, we don't need to do all the housekeeping work, as it - // will be reset when the keyboard shows up anyway. - // TODO: revisit this when LatinIME supports hardware keyboards. - // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). - // TODO: find a better way to simulate actual execution. - if (isInputViewShown() && - mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)) { + // This call happens when we have a hardware keyboard as well as when we don't. While we + // don't support hardware keyboards yet we should avoid doing the processing associated + // with cursor movement when we have a hardware keyboard since we are not in charge. + final SettingsValues settingsValues = mSettings.getCurrent(); + if ((!settingsValues.mHasHardwareKeyboard || ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) + && mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, + settingsValues)) { mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } @@ -971,11 +1043,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // We cannot mark this method as @Override until new SDK becomes publicly available. // @Override public void onUpdateCursorAnchorInfo(final CursorAnchorInfo info) { - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { - final CursorAnchorInfoCompatWrapper wrapper = - CursorAnchorInfoCompatWrapper.fromObject(info); - // TODO: Implement here + if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || isFullscreenMode()) { + return; } + mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); } /** @@ -1051,108 +1122,73 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen applicationSpecifiedCompletions); final SuggestedWords suggestedWords = new SuggestedWords(applicationSuggestedWords, null /* rawSuggestions */, false /* typedWordValid */, false /* willAutoCorrect */, - false /* isObsoleteSuggestions */, false /* isPrediction */, + false /* isObsoleteSuggestions */, SuggestedWords.INPUT_STYLE_APPLICATION_SPECIFIED /* inputStyle */); // When in fullscreen mode, show completions generated by the application forcibly setSuggestedWords(suggestedWords); } - private int getAdjustedBackingViewHeight() { - final int currentHeight = mKeyPreviewBackingView.getHeight(); - if (currentHeight > 0) { - return currentHeight; - } - - final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); - if (visibleKeyboardView == null) { - return 0; - } - // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main !!! - // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - final int keyboardHeight = visibleKeyboardView.getHeight(); - final int suggestionsHeight = mSuggestionStripView.getHeight(); - final int displayHeight = getResources().getDisplayMetrics().heightPixels; - final Rect rect = new Rect(); - mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); - final int notificationBarHeight = rect.top; - final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight - - keyboardHeight; - - final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); - mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); - - // Let the backing cover the remaining region entirely. - params.height = remainingHeight; - mKeyPreviewBackingView.setLayoutParams(params); - return params.height; - } - @Override public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); + final SettingsValues settingsValues = mSettings.getCurrent(); final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); if (visibleKeyboardView == null || !hasSuggestionStripView()) { return; } - final boolean hasHardwareKeyboard = mKeyboardSwitcher.hasHardwareKeyboard(); + final int inputHeight = mInputView.getHeight(); + final boolean hasHardwareKeyboard = settingsValues.mHasHardwareKeyboard; if (hasHardwareKeyboard && visibleKeyboardView.getVisibility() == View.GONE) { // If there is a hardware keyboard and a visible software keyboard view has been hidden, // no visual element will be shown on the screen. - outInsets.touchableInsets = mInputView.getHeight(); - outInsets.visibleTopInsets = mInputView.getHeight(); + outInsets.touchableInsets = inputHeight; + outInsets.visibleTopInsets = inputHeight; return; } - final int adjustedBackingHeight = getAdjustedBackingViewHeight(); - final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE); - final int backingHeight = backingGone ? 0 : adjustedBackingHeight; - // In fullscreen mode, the height of the extract area managed by InputMethodService should - // be considered. - // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. - final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; - final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0 - : mSuggestionStripView.getHeight(); - final int extraHeight = extractHeight + backingHeight + suggestionsHeight; - int visibleTopY = extraHeight; - // Need to set touchable region only if input view is being shown + final int suggestionsHeight = (!mKeyboardSwitcher.isShowingEmojiPalettes() + && mSuggestionStripView.getVisibility() == View.VISIBLE) + ? mSuggestionStripView.getHeight() : 0; + final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - suggestionsHeight; + mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY); + // Need to set touchable region only if a keyboard view is being shown. if (visibleKeyboardView.isShown()) { - // Note that the height of Emoji layout is the same as the height of the main keyboard - // and the suggestion strip - if (mKeyboardSwitcher.isShowingEmojiPalettes() - || mSuggestionStripView.getVisibility() == View.VISIBLE) { - visibleTopY -= suggestionsHeight; - } - final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; - final int touchWidth = visibleKeyboardView.getWidth(); - final int touchHeight = visibleKeyboardView.getHeight() + extraHeight + final int touchLeft = 0; + final int touchTop = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; + final int touchRight = visibleKeyboardView.getWidth(); + final int touchBottom = inputHeight // Extend touchable region below the keyboard. + EXTENDED_TOUCHABLE_REGION_HEIGHT; outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; - outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); + outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom); } outInsets.contentTopInsets = visibleTopY; outInsets.visibleTopInsets = visibleTopY; } - @Override - public boolean onEvaluateInputViewShown() { - // Always show {@link InputView}. - return true; + public void startShowingInputView() { + mIsExecutingStartShowingInputView = true; + // This {@link #showWindow(boolean)} will eventually call back + // {@link #onEvaluateInputViewShown()}. + showWindow(true /* showInput */); + mIsExecutingStartShowingInputView = false; + } + + public void stopShowingInputView() { + showWindow(false /* showInput */); } @Override - public boolean onShowInputRequested(final int flags, final boolean configChange) { - if ((flags & InputMethod.SHOW_EXPLICIT) == 0 && mKeyboardSwitcher.hasHardwareKeyboard()) { - // Even when IME is implicitly shown and physical keyboard is connected, we should - // show {@link InputView}. - // See {@link InputMethodService#onShowInputRequested(int,boolean)}. + public boolean onEvaluateInputViewShown() { + if (mIsExecutingStartShowingInputView) { return true; } - return super.onShowInputRequested(flags, configChange); + return super.onEvaluateInputViewShown(); } @Override public boolean onEvaluateFullscreenMode() { - if (mKeyboardSwitcher.hasHardwareKeyboard()) { + final SettingsValues settingsValues = mSettings.getCurrent(); + if (settingsValues.mHasHardwareKeyboard) { // If there is a hardware keyboard, disable full screen mode. return false; } @@ -1165,19 +1201,34 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // hack for now. Let's get rid of this once the framework gets fixed. final EditorInfo ei = getCurrentInputEditorInfo(); return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0)); - } else { - return false; } + return false; } @Override public void updateFullscreenMode() { + // Override layout parameters to expand {@link SoftInputWindow} to the entire screen. + // See {@link InputMethodService#setinputView(View) and + // {@link SoftInputWindow#updateWidthHeight(WindowManager.LayoutParams)}. + final Window window = getWindow().getWindow(); + ViewLayoutUtils.updateLayoutHeightOf(window, LayoutParams.MATCH_PARENT); + // This method may be called before {@link #setInputView(View)}. + if (mInputView != null) { + // In non-fullscreen mode, {@link InputView} and its parent inputArea should expand to + // the entire screen and be placed at the bottom of {@link SoftInputWindow}. + // In fullscreen mode, these shouldn't expand to the entire screen and should be + // coexistent with {@link #mExtractedArea} above. + // See {@link InputMethodService#setInputView(View) and + // com.android.internal.R.layout.input_method.xml. + final int layoutHeight = isFullscreenMode() + ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; + final View inputArea = window.findViewById(android.R.id.inputArea); + ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight); + ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM); + ViewLayoutUtils.updateLayoutHeightOf(mInputView, layoutHeight); + } super.updateFullscreenMode(); - - if (mKeyPreviewBackingView == null) return; - // In fullscreen mode, no need to have extra space to show the key preview. - // If not, we should have extra space above the keyboard to show the key preview. - mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); + mInputLogic.onUpdateFullscreenMode(isFullscreenMode()); } private int getCurrentAutoCapsState() { @@ -1201,9 +1252,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (null == keyboard) { return CoordinateUtils.newCoordinateArray(codePoints.length, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } else { - return keyboard.getCoordinates(codePoints); } + return keyboard.getCoordinates(codePoints); } // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is @@ -1221,6 +1271,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen wordToEdit = word; } mDictionaryFacilitator.addWordToUserDictionary(this /* context */, wordToEdit); + mInputLogic.onAddWordToUserDictionary(); } // Callback for the {@link SuggestionStripView}, to call when the important notice strip is @@ -1409,7 +1460,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } private void setSuggestedWords(final SuggestedWords suggestedWords) { - mInputLogic.setSuggestedWords(suggestedWords); + final SettingsValues currentSettingsValues = mSettings.getCurrent(); + mInputLogic.setSuggestedWords(suggestedWords, currentSettingsValues, mHandler); // TODO: Modify this when we support suggestions with hard keyboard if (!hasSuggestionStripView()) { return; @@ -1418,7 +1470,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } - final SettingsValues currentSettingsValues = mSettings.getCurrent(); final boolean shouldShowImportantNotice = ImportantNoticeUtils.shouldShowImportantNotice(this); final boolean shouldShowSuggestionCandidates = @@ -1438,19 +1489,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/PunctuationSuggestions.java b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java index 6b0205c0f..56014cbad 100644 --- a/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java +++ b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java @@ -35,7 +35,6 @@ public final class PunctuationSuggestions extends SuggestedWords { false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, false /* isObsoleteSuggestions */, - false /* isPrediction */, INPUT_STYLE_NONE /* inputStyle */); } diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 035557610..744b0321a 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -30,7 +30,9 @@ import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import com.android.inputmethod.compat.InputConnectionCompatUtils; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.DebugLogUtils; @@ -250,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()); } /** @@ -263,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); @@ -283,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; @@ -621,14 +626,24 @@ public final class RichInputConnection { return Arrays.binarySearch(sortedSeparators, code) >= 0; } + private static boolean isPartOfCompositionForScript(final int codePoint, + final SpacingAndPunctuations spacingAndPunctuations, final int scriptId) { + // We always consider word connectors part of compositions. + return spacingAndPunctuations.isWordConnector(codePoint) + // Otherwise, it's part of composition if it's part of script and not a separator. + || (!spacingAndPunctuations.isWordSeparator(codePoint) + && ScriptUtils.isLetterPartOfScript(codePoint, scriptId)); + } + /** * Returns the text surrounding the cursor. * - * @param sortedSeparators a sorted array of code points that split words. + * @param spacingAndPunctuations the rules for spacing and punctuation * @param scriptId the script we consider to be writing words, as one of ScriptUtils.SCRIPT_* * @return a range containing the text surrounding the cursor */ - public TextRange getWordRangeAtCursor(final int[] sortedSeparators, final int scriptId) { + public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations, + final int scriptId) { mIC = mParent.getCurrentInputConnection(); if (mIC == null) { return null; @@ -645,8 +660,7 @@ public final class RichInputConnection { int startIndexInBefore = before.length(); while (startIndexInBefore > 0) { final int codePoint = Character.codePointBefore(before, startIndexInBefore); - if (isSeparator(codePoint, sortedSeparators) - || !ScriptUtils.isLetterPartOfScript(codePoint, scriptId)) { + if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) { break; } --startIndexInBefore; @@ -659,8 +673,7 @@ public final class RichInputConnection { int endIndexInAfter = -1; while (++endIndexInAfter < after.length()) { final int codePoint = Character.codePointAt(after, endIndexInAfter); - if (isSeparator(codePoint, sortedSeparators) - || !ScriptUtils.isLetterPartOfScript(codePoint, scriptId)) { + if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) { break; } if (Character.isSupplementaryCodePoint(codePoint)) { @@ -906,4 +919,39 @@ public final class RichInputConnection { mIC.setSelection(mExpectedSelStart, mExpectedSelEnd); } } + + private boolean mCursorAnchorInfoMonitorEnabled = false; + + /** + * Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}. + * @param enableMonitor {@code true} to request the editor to call back the method whenever the + * cursor/anchor position is changed. + * @param requestImmediateCallback {@code true} to request the editor to call back the method + * as soon as possible to notify the current cursor/anchor position to the input method. + * @return {@code true} if the request is accepted. Returns {@code false} otherwise, which + * includes "not implemented" or "rejected" or "temporarily unavailable" or whatever which + * prevents the application from fulfilling the request. (TODO: Improve the API when it turns + * out that we actually need more detailed error codes) + */ + public boolean requestCursorUpdates(final boolean enableMonitor, + final boolean requestImmediateCallback) { + mIC = mParent.getCurrentInputConnection(); + final boolean scheduled; + if (null != mIC) { + scheduled = InputConnectionCompatUtils.requestCursorUpdates(mIC, enableMonitor, + requestImmediateCallback); + } else { + scheduled = false; + } + mCursorAnchorInfoMonitorEnabled = (scheduled && enableMonitor); + return scheduled; + } + + /** + * @return {@code true} if the application reported that the monitor mode of + * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is currently enabled. + */ + public boolean isCursorAnchorInfoMonitorEnabled() { + return mCursorAnchorInfoMonitorEnabled; + } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index ab852f8dd..b03818c1d 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -120,9 +120,9 @@ public final class Suggest { // and calls the callback function with the suggestions. private void getSuggestedWordsForNonBatchInput(final WordComposer wordComposer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, - final SettingsValuesForSuggestion settingsValuesForSuggestion, final int inputStyle, - final boolean isCorrectionEnabled, final int sequenceNumber, - final OnGetSuggestedWordsCallback callback) { + final SettingsValuesForSuggestion settingsValuesForSuggestion, + final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled, + final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { final String typedWord = wordComposer.getTypedWord(); final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(typedWord); final String consideredWord = trailingSingleQuotesCount > 0 @@ -186,6 +186,14 @@ public final class Suggest { suggestionsList = suggestionsContainer; } + 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 @@ -193,8 +201,7 @@ public final class Suggest { // rename the attribute or change the value. !resultsArePredictions && !allowsToBeAutoCorrected /* typedWordValid */, hasAutoCorrection /* willAutoCorrect */, - false /* isObsoleteSuggestions */, resultsArePredictions, - inputStyle, sequenceNumber)); + false /* isObsoleteSuggestions */, inputStyle, sequenceNumber)); } // Retrieves suggestions for the batch input @@ -239,12 +246,13 @@ 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 */, false /* willAutoCorrect */, false /* isObsoleteSuggestions */, - false /* isPrediction */, inputStyle, sequenceNumber)); } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 38fcb683d..1d221b77f 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -38,14 +38,16 @@ public class SuggestedWords { public static final int INPUT_STYLE_TAIL_BATCH = 3; 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; private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0); public static final SuggestedWords EMPTY = new SuggestedWords( - EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false, false, false, false, - INPUT_STYLE_NONE); + EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false /* typedWordValid */, + false /* willAutoCorrect */, false /* isObsoleteSuggestions */, INPUT_STYLE_NONE); public final String mTypedWord; public final boolean mTypedWordValid; @@ -54,7 +56,6 @@ public class SuggestedWords { // whether this exactly matches the user entry or not. public final boolean mWillAutoCorrect; public final boolean mIsObsoleteSuggestions; - public final boolean mIsPrediction; // How the input for these suggested words was done by the user. Must be one of the // INPUT_STYLE_* constants above. public final int mInputStyle; @@ -67,10 +68,9 @@ public class SuggestedWords { final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, - final boolean isPrediction, final int inputStyle) { this(suggestedWordInfoList, rawSuggestions, typedWordValid, willAutoCorrect, - isObsoleteSuggestions, isPrediction, inputStyle, NOT_A_SEQUENCE_NUMBER); + isObsoleteSuggestions, inputStyle, NOT_A_SEQUENCE_NUMBER); } public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, @@ -78,14 +78,12 @@ public class SuggestedWords { final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, - final boolean isPrediction, final int inputStyle, final int sequenceNumber) { this(suggestedWordInfoList, rawSuggestions, - (suggestedWordInfoList.isEmpty() || isPrediction) ? null + (suggestedWordInfoList.isEmpty() || isPrediction(inputStyle)) ? null : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord, - typedWordValid, willAutoCorrect, isObsoleteSuggestions, isPrediction, inputStyle, - sequenceNumber); + typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, sequenceNumber); } public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, @@ -94,7 +92,6 @@ public class SuggestedWords { final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, - final boolean isPrediction, final int inputStyle, final int sequenceNumber) { mSuggestedWordInfoList = suggestedWordInfoList; @@ -102,7 +99,6 @@ public class SuggestedWords { mTypedWordValid = typedWordValid; mWillAutoCorrect = willAutoCorrect; mIsObsoleteSuggestions = isObsoleteSuggestions; - mIsPrediction = isPrediction; mInputStyle = inputStyle; mSequenceNumber = sequenceNumber; mTypedWord = typedWord; @@ -175,6 +171,7 @@ public class SuggestedWords { return "SuggestedWords:" + " mTypedWordValid=" + mTypedWordValid + " mWillAutoCorrect=" + mWillAutoCorrect + + " mInputStyle=" + mInputStyle + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray()); } @@ -381,9 +378,19 @@ 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 isPrediction(mInputStyle); + } + // SuggestedWords is an immutable object, as much as possible. We must not just remove // words from the member ArrayList as some other parties may expect the object to never change. - public SuggestedWords getSuggestedWordsExcludingTypedWord(final int inputStyle) { + // This is only ever called by recorrection at the moment, hence the ForRecorrection moniker. + public SuggestedWords getSuggestedWordsExcludingTypedWordForRecorrection() { final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>(); String typedWord = null; for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { @@ -399,7 +406,7 @@ public class SuggestedWords { // no auto-correction should take place hence willAutoCorrect = false. return new SuggestedWords(newSuggestions, null /* rawSuggestions */, typedWord, true /* typedWordValid */, false /* willAutoCorrect */, mIsObsoleteSuggestions, - mIsPrediction, inputStyle, NOT_A_SEQUENCE_NUMBER); + SuggestedWords.INPUT_STYLE_RECORRECTION, NOT_A_SEQUENCE_NUMBER); } // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the @@ -418,8 +425,7 @@ public class SuggestedWords { SuggestedWordInfo.NOT_A_CONFIDENCE)); } return new SuggestedWords(newSuggestions, null /* rawSuggestions */, mTypedWordValid, - mWillAutoCorrect, mIsObsoleteSuggestions, mIsPrediction, - INPUT_STYLE_TAIL_BATCH); + mWillAutoCorrect, mIsObsoleteSuggestions, INPUT_STYLE_TAIL_BATCH); } /** @@ -429,7 +435,7 @@ public class SuggestedWords { */ @UsedForTesting public SuggestedWordInfo getTypedWordInfoOrNull() { - if (this == EMPTY) { + if (SuggestedWords.INDEX_OF_TYPED_WORD >= size()) { return null; } final SuggestedWordInfo info = getInfo(SuggestedWords.INDEX_OF_TYPED_WORD); diff --git a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java index e4ee42660..123ab208c 100644 --- a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java @@ -17,21 +17,16 @@ package com.android.inputmethod.latin; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.os.Process; -import android.preference.PreferenceManager; import android.util.Log; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.IntentCompatUtils; -import com.android.inputmethod.latin.settings.Settings; +import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.latin.setup.LauncherIconVisibilityManager; -import com.android.inputmethod.latin.setup.SetupActivity; import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; /** @@ -58,6 +53,9 @@ import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; * When a multiuser account has been created, {@link Intent#ACTION_USER_INITIALIZE} is received * by this receiver and it checks the whether the setup wizard's icon should be appeared or not on * the launcher depending on which partition this IME is installed. + * + * When the system locale has been changed, {@link Intent#ACTION_LOCALE_CHANGED} is received by + * this receiver and the {@link KeyboardLayoutSet}'s cache is cleared. */ public final class SystemBroadcastReceiver extends BroadcastReceiver { private static final String TAG = SystemBroadcastReceiver.class.getSimpleName(); @@ -67,21 +65,22 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { final String intentAction = intent.getAction(); if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intentAction)) { Log.i(TAG, "Package has been replaced: " + context.getPackageName()); - } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { - Log.i(TAG, "Boot has been completed"); - } else if (IntentCompatUtils.is_ACTION_USER_INITIALIZE(intentAction)) { - Log.i(TAG, "User initialize"); - } - - LauncherIconVisibilityManager.onReceiveGlobalIntent(intentAction, context); - - if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intentAction)) { // Need to restore additional subtypes because system always clears additional // subtypes when the package is replaced. RichInputMethodManager.init(context); final RichInputMethodManager richImm = RichInputMethodManager.getInstance(); final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(context); richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); + LauncherIconVisibilityManager.updateSetupWizardIconVisibility(context); + } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { + Log.i(TAG, "Boot has been completed"); + LauncherIconVisibilityManager.updateSetupWizardIconVisibility(context); + } else if (IntentCompatUtils.is_ACTION_USER_INITIALIZE(intentAction)) { + Log.i(TAG, "User initialize"); + LauncherIconVisibilityManager.updateSetupWizardIconVisibility(context); + } else if (Intent.ACTION_LOCALE_CHANGED.equals(intentAction)) { + Log.i(TAG, "System locale changed"); + KeyboardLayoutSet.onSystemLocaleChanged(); } // The process that hosts this broadcast receiver is invoked and remains alive even after diff --git a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java index 7071d8689..a87785b1a 100644 --- a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java +++ b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java @@ -168,7 +168,7 @@ public class ExternalDictionaryGetterForDebug { } catch (IOException e) { // There was an error: show a dialog new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) - .setTitle(R.string.error) + .setTitle(R.string.read_external_dictionary_error) .setMessage(e.toString()) .setPositiveButton(android.R.string.ok, new OnClickListener() { @Override diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 616828efe..fdab7f25f 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -17,21 +17,28 @@ package com.android.inputmethod.latin.inputlogic; import android.graphics.Color; +import android.inputmethodservice.InputMethodService; import android.os.SystemClock; import android.text.SpannableString; +import android.text.Spanned; import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; import android.text.style.SuggestionSpan; 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; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.event.Event; import com.android.inputmethod.event.InputTransaction; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.keyboard.TextDecorator; +import com.android.inputmethod.keyboard.TextDecoratorUiOperator; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryFacilitator; @@ -46,6 +53,7 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.define.DebugFlags; +import com.android.inputmethod.latin.define.ProductionFlags; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; @@ -81,6 +89,14 @@ public final class InputLogic { public final Suggest mSuggest; private final DictionaryFacilitator mDictionaryFacilitator; + private final TextDecorator mTextDecorator = new TextDecorator(new TextDecorator.Listener() { + @Override + public void onClickComposingTextToAddToDictionary(final String word) { + mLatinIME.addWordToUserDictionary(word); + mLatinIME.dismissAddToDictionaryHint(); + } + }); + public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; // This has package visibility so it can be accessed from InputLogicHandler. /* package */ final WordComposer mWordComposer; @@ -124,8 +140,9 @@ public final class InputLogic { * Call this when input starts or restarts in some editor (typically, in onStartInputView). * * @param combiningSpec the combining spec string for this subtype + * @param settingsValues the current settings values */ - public void startInput(final String combiningSpec) { + public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; mWordComposer.restartCombining(combiningSpec); resetComposingState(true /* alsoResetLastComposedWord */); @@ -143,15 +160,25 @@ public final class InputLogic { } else { mInputLogicHandler.reset(); } + + if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { + // AcceptTypedWord feature relies on CursorAnchorInfo. + if (settingsValues.mShouldShowUiToAcceptTypedWord) { + mConnection.requestCursorUpdates(true /* enableMonitor */, + true /* requestImmediateCallback */); + } + mTextDecorator.reset(); + } } /** * Call this when the subtype changes. * @param combiningSpec the spec string for the combining rules + * @param settingsValues the current settings values */ - public void onSubtypeChanged(final String combiningSpec) { + public void onSubtypeChanged(final String combiningSpec, final SettingsValues settingsValues) { finishInput(); - startInput(combiningSpec); + startInput(combiningSpec, settingsValues); } /** @@ -303,8 +330,9 @@ public final class InputLogic { return inputTransaction; } - commitChosenWord(settingsValues, suggestion, - LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); + final boolean shouldShowAddToDictionaryHint = shouldShowAddToDictionaryHint(suggestionInfo); + commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, + LastComposedWord.NOT_A_SEPARATOR); mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); @@ -312,7 +340,7 @@ public final class InputLogic { mSpaceState = SpaceState.PHANTOM; inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - if (shouldShowAddToDictionaryHint(suggestionInfo)) { + if (shouldShowAddToDictionaryHint) { mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); } else { // If we're not showing the "Touch again to save", then update the suggestion strip. @@ -330,10 +358,11 @@ public final class InputLogic { * @param oldSelEnd old selection end * @param newSelStart new selection start * @param newSelEnd new selection end + * @param settingsValues the current values of the settings. * @return whether the cursor has moved as a result of user interaction. */ public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd, - final int newSelStart, final int newSelEnd) { + final int newSelStart, final int newSelEnd, final SettingsValues settingsValues) { if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) { return false; } @@ -358,8 +387,9 @@ public final class InputLogic { // should be true, but that is if the framework had taken that wrong cursor position // into account, which means we have to reset the entire composing state whenever there // is or was a selection regardless of whether it changed or not. - if (hasOrHadSelection || (selectionChangedOrSafeToReset - && !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { + if (hasOrHadSelection || !settingsValues.needsToLookupSuggestions() + || (selectionChangedOrSafeToReset + && !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { // If we are composing a word and moving the cursor, we would want to set a // suggestion span for recorrection to work correctly. Unfortunately, that // would involve the keyboard committing some new text, which would move the @@ -386,6 +416,11 @@ public final class InputLogic { // The cursor has been moved : we now accept to perform recapitalization 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 */); @@ -464,7 +499,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. @@ -561,7 +598,8 @@ public final class InputLogic { // TODO: on the long term, this method should become private, but it will be difficult. // Especially, how do we deal with InputMethodService.onDisplayCompletions? - public void setSuggestedWords(final SuggestedWords suggestedWords) { + public void setSuggestedWords(final SuggestedWords suggestedWords, + final SettingsValues settingsValues, final LatinIME.UIHandler handler) { if (SuggestedWords.EMPTY != suggestedWords) { final String autoCorrection; if (suggestedWords.mWillAutoCorrect) { @@ -575,6 +613,7 @@ public final class InputLogic { } mSuggestedWords = suggestedWords; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; + // Put a blue underline to a word in TextView which will be auto-corrected. if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator && mWordComposer.isComposingWord()) { @@ -585,7 +624,7 @@ public final class InputLogic { // message, this is called outside any batch edit. Potentially, this may result in some // janky flickering of the screen, although the display speed makes it unlikely in // the practice. - mConnection.setComposingText(textWithUnderline, 1); + setComposingTextInternal(textWithUnderline, 1); } } @@ -608,7 +647,7 @@ public final class InputLogic { inputTransaction.setDidAffectContents(); } if (mWordComposer.isComposingWord()) { - mConnection.setComposingText(mWordComposer.getTypedWord(), 1); + setComposingTextInternal(mWordComposer.getTypedWord(), 1); inputTransaction.setDidAffectContents(); inputTransaction.setRequiresUpdateSuggestions(); } @@ -638,7 +677,7 @@ public final class InputLogic { case Constants.CODE_SHIFT: performRecapitalization(inputTransaction.mSettingsValues); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - if (mSuggestedWords.mIsPrediction) { + if (mSuggestedWords.isPrediction()) { inputTransaction.setRequiresUpdateSuggestions(); } break; @@ -751,11 +790,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(); + // 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; @@ -842,8 +884,7 @@ public final class InputLogic { if (mWordComposer.isSingleLetter()) { mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState); } - mConnection.setComposingText(getTextWithUnderline( - mWordComposer.getTypedWord()), 1); + setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } else { final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event, inputTransaction); @@ -1006,14 +1047,14 @@ public final class InputLogic { mWordComposer.applyProcessedEvent(event); } if (mWordComposer.isComposingWord()) { - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } else { mConnection.commitText("", 1); } inputTransaction.setRequiresUpdateSuggestions(); } else { if (mLastComposedWord.canRevertCommit()) { - revertCommit(inputTransaction); + revertCommit(inputTransaction, inputTransaction.mSettingsValues); return; } if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { @@ -1395,8 +1436,7 @@ public final class InputLogic { return; } final TextRange range = mConnection.getWordRangeAtCursor( - settingsValues.mSpacingAndPunctuations.mSortedWordSeparators, - currentKeyboardScriptId); + settingsValues.mSpacingAndPunctuations, currentKeyboardScriptId); if (null == range) return; // Happens if we don't have an input connection at all if (range.length() <= 0) { // Race condition, or touching a word in a non-supported script. @@ -1466,11 +1506,10 @@ public final class InputLogic { && !shouldIncludeResumedWordInSuggestions) { // We were able to compute new suggestions for this word. // Remove the typed word, since we don't want to display it in this - // case. The #getSuggestedWordsExcludingTypedWord() method sets - // willAutoCorrect to false. + // case. The #getSuggestedWordsExcludingTypedWordForRecorrection() + // method sets willAutoCorrect to false. suggestedWords = suggestedWordsIncludingTypedWord - .getSuggestedWordsExcludingTypedWord(SuggestedWords - .INPUT_STYLE_RECORRECTION); + .getSuggestedWordsExcludingTypedWordForRecorrection(); } else { // No saved suggestions, and we were unable to compute any good one // either. Rather than displaying an empty suggestion strip, we'll @@ -1487,11 +1526,9 @@ public final class InputLogic { // color of the word in the suggestion strip changes according to this parameter, // and false gives the correct color. final SuggestedWords suggestedWords = new SuggestedWords(suggestions, - null /* rawSuggestions */, typedWord, - false /* typedWordValid */, false /* willAutoCorrect */, - false /* isObsoleteSuggestions */, false /* isPrediction */, - SuggestedWords.INPUT_STYLE_RECORRECTION, - SuggestedWords.NOT_A_SEQUENCE_NUMBER); + null /* rawSuggestions */, typedWord, false /* typedWordValid */, + false /* willAutoCorrect */, false /* isObsoleteSuggestions */, + SuggestedWords.INPUT_STYLE_RECORRECTION, SuggestedWords.NOT_A_SEQUENCE_NUMBER); mIsAutoCorrectionIndicatorOn = false; mLatinIME.mHandler.showSuggestionStrip(suggestedWords); } @@ -1503,14 +1540,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) { @@ -1529,7 +1571,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; @@ -1566,23 +1608,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)); - mConnection.setComposingText(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(); + } } /** @@ -1787,8 +1859,7 @@ public final class InputLogic { SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords); return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, - true /* isObsoleteSuggestions */, false /* isPrediction */, - oldSuggestedWords.mInputStyle); + true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle); } /** @@ -1911,10 +1982,10 @@ public final class InputLogic { } final String lastWord = batchInputText.substring(indexOfLastSpace); mWordComposer.setBatchInputWord(lastWord); - mConnection.setComposingText(lastWord, 1); + setComposingTextInternal(lastWord, 1); } else { mWordComposer.setBatchInputWord(batchInputText); - mConnection.setComposingText(batchInputText, 1); + setComposingTextInternal(batchInputText, 1); } mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState @@ -2007,9 +2078,7 @@ public final class InputLogic { } /** - * 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. @@ -2018,23 +2087,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, @@ -2044,7 +2096,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 @@ -2112,4 +2164,122 @@ public final class InputLogic { settingsValues.mAutoCorrectionEnabledPerUserSettings, inputStyle, sequenceNumber, callback); } + + /** + * Used as an injection point for each call of + * {@link RichInputConnection#setComposingText(CharSequence, int)}. + * + * <p>Currently using this method is optional and you can still directly call + * {@link RichInputConnection#setComposingText(CharSequence, int)}, but it is recommended to + * use this method whenever possible to optimize the behavior of {@link TextDecorator}.<p> + * <p>TODO: Should we move this mechanism to {@link RichInputConnection}?</p> + * + * @param newComposingText the composing text to be set + * @param newCursorPosition the new cursor position + */ + private void setComposingTextInternal(final CharSequence newComposingText, + final int newCursorPosition) { + setComposingTextInternalWithBackgroundColor(newComposingText, newCursorPosition, + Color.TRANSPARENT, newComposingText.length()); + } + + /** + * Equivalent to {@link #setComposingTextInternal(CharSequence, int)} except that this method + * allows to set {@link BackgroundColorSpan} to the composing text with the given color. + * + * <p>TODO: Currently the background color is exclusive with the black underline, which is + * automatically added by the framework. We need to change the framework if we need to have both + * of them at the same time.</p> + * <p>TODO: Should we move this method to {@link RichInputConnection}?</p> + * + * @param newComposingText the composing text to be set + * @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 coloredTextLength) { + final CharSequence composingTextToBeSet; + if (backgroundColor == Color.TRANSPARENT) { + composingTextToBeSet = newComposingText; + } else { + final SpannableString spannable = new SpannableString(newComposingText); + final BackgroundColorSpan backgroundColorSpan = + new BackgroundColorSpan(backgroundColor); + final int spanLength = Math.min(coloredTextLength, spannable.length()); + spannable.setSpan(backgroundColorSpan, 0, spanLength, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); + composingTextToBeSet = spannable; + } + mConnection.setComposingText(composingTextToBeSet, newCursorPosition); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + // Following methods are tentatively placed in this class for the integration with + // TextDecorator. + // TODO: Decouple things that are not related to the input logic. + ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Sets the UI operator for {@link TextDecorator}. + * @param uiOperator the UI operator which should be associated with {@link TextDecorator}. + */ + public void setTextDecoratorUi(final TextDecoratorUiOperator uiOperator) { + mTextDecorator.setUiOperator(uiOperator); + } + + /** + * 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) { + mTextDecorator.onUpdateCursorAnchorInfo(info); + } + + /** + * Must be called when {@link InputMethodService#updateFullscreenMode} is called. + * @param isFullscreen {@code true} if the input method is in full-screen mode. + */ + public void onUpdateFullscreenMode(final boolean isFullscreen) { + mTextDecorator.notifyFullScreenMode(isFullscreen); + } + + /** + * Must be called from {@link LatinIME#addWordToUserDictionary(String)}. + */ + public void onAddWordToUserDictionary() { + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); + mTextDecorator.reset(); + } + + /** + * 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 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. + return false; + } + if (!settingsValues.mShouldShowUiToAcceptTypedWord) { + return false; + } + if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) { + return false; + } + if (TextUtils.equals(lastComposedWord.mTypedWord, lastComposedWord.mCommittedWord)) { + return false; + } + if (!mDictionaryFacilitator.isUserDictionaryEnabled()) { + return false; + } + return !mDictionaryFacilitator.isValidWord(lastComposedWord.mTypedWord, + true /* ignoreCase */); + } } diff --git a/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java new file mode 100644 index 000000000..f5e4d33a2 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java @@ -0,0 +1,41 @@ +/* + * 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.os.Bundle; + +import com.android.inputmethod.latin.R; + + +/** + * "Appearance" settings sub screen. + */ +public final class AppearanceSettingsFragment extends SubScreenFragment { + @Override + public void onCreate(final Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.prefs_screen_appearance); + } + + @Override + public void onResume() { + super.onResume(); + CustomInputStyleSettingsFragment.updateCustomInputStylesSummary( + findPreference(Settings.PREF_CUSTOM_INPUT_STYLES)); + ThemeSettingsFragment.updateKeyboardThemeSummary(findPreference(Settings.SCREEN_THEME)); + } +} diff --git a/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java index 6d7f53cf0..9bc398654 100644 --- a/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java @@ -30,11 +30,15 @@ import android.preference.DialogPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceGroup; +import android.support.v4.view.ViewCompat; +import android.text.TextUtils; import android.util.Pair; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.widget.ArrayAdapter; @@ -43,6 +47,7 @@ import android.widget.SpinnerAdapter; import android.widget.Toast; import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; +import com.android.inputmethod.compat.ViewCompatUtils; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; @@ -233,6 +238,12 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { mSubtypeLocaleSpinner.setAdapter(mProxy.getSubtypeLocaleAdapter()); mKeyboardLayoutSetSpinner = (Spinner) v.findViewById(R.id.keyboard_layout_set_spinner); mKeyboardLayoutSetSpinner.setAdapter(mProxy.getKeyboardLayoutSetAdapter()); + // All keyboard layout names are in the Latin script and thus left to right. That means + // the view would align them to the left even if the system locale is RTL, but that + // would look strange. To fix this, we align them to the view's start, which will be + // natural for any direction. + ViewCompatUtils.setTextAlignment( + mKeyboardLayoutSetSpinner, ViewCompatUtils.TEXT_ALIGNMENT_VIEW_START); return v; } @@ -386,6 +397,25 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { // Empty constructor for fragment generation. } + static void updateCustomInputStylesSummary(final Preference pref) { + // 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()}. + SubtypeLocaleUtils.init(pref.getContext()); + + final Resources res = pref.getContext().getResources(); + final SharedPreferences prefs = pref.getSharedPreferences(); + final String prefSubtype = Settings.readPrefAdditionalSubtypes(prefs, res); + final InputMethodSubtype[] subtypes = + AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype); + final ArrayList<String> subtypeNames = new ArrayList<>(); + for (final InputMethodSubtype subtype : subtypes) { + subtypeNames.add(SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)); + } + // TODO: A delimiter of custom input styles should be localized. + pref.setSummary(TextUtils.join(", ", subtypeNames)); + } + @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -398,6 +428,16 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { } @Override + public View onCreateView(final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + // For correct display in RTL locales, we need to set the layout direction of the + // fragment's top view. + ViewCompat.setLayoutDirection(view, ViewCompat.LAYOUT_DIRECTION_LOCALE); + return view; + } + + @Override public void onActivityCreated(final Bundle savedInstanceState) { final Context context = getActivity(); mSubtypeLocaleAdapter = new SubtypeLocaleAdapter(context); @@ -422,7 +462,7 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { KEY_SUBTYPE_FOR_SUBTYPE_ENABLER); final SubtypePreference subtypePref = (SubtypePreference)findPreference( mSubtypePreferenceKeyForSubtypeEnabler); - mSubtypeEnablerNotificationDialog = createDialog(subtypePref); + mSubtypeEnablerNotificationDialog = createDialog(); mSubtypeEnablerNotificationDialog.show(); } } @@ -476,7 +516,7 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { if (findDuplicatedSubtype(subtype) == null) { mRichImm.setAdditionalInputMethodSubtypes(getSubtypes()); mSubtypePreferenceKeyForSubtypeEnabler = subtypePref.getKey(); - mSubtypeEnablerNotificationDialog = createDialog(subtypePref); + mSubtypeEnablerNotificationDialog = createDialog(); mSubtypeEnablerNotificationDialog.show(); return; } @@ -513,7 +553,7 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { localeString, keyboardLayoutSetName); } - private AlertDialog createDialog(final SubtypePreference subtypePref) { + private AlertDialog createDialog() { final AlertDialog.Builder builder = new AlertDialog.Builder( DialogUtils.getPlatformDialogThemeContext(getActivity())); builder.setTitle(R.string.custom_input_styles_title) diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java index 63d848e2d..48f4c758c 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java @@ -23,10 +23,16 @@ public final class DebugSettings { "force_physical_keyboard_special_key"; public static final String PREF_SHOW_UI_TO_ACCEPT_TYPED_WORD = "pref_show_ui_to_accept_typed_word"; - public static final String PREF_KEY_PREVIEW_SHOW_UP_START_SCALE = - "pref_key_preview_show_up_start_scale"; - public static final String PREF_KEY_PREVIEW_DISMISS_END_SCALE = - "pref_key_preview_dismiss_end_scale"; + public static final String PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS = + "pref_has_custom_key_preview_animation_params"; + public static final String PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE = + "pref_key_preview_show_up_start_x_scale"; + public static final String PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE = + "pref_key_preview_show_up_start_y_scale"; + public static final String PREF_KEY_PREVIEW_DISMISS_END_X_SCALE = + "pref_key_preview_dismiss_end_x_scale"; + public static final String PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE = + "pref_key_preview_dismiss_end_y_scale"; public static final String PREF_KEY_PREVIEW_SHOW_UP_DURATION = "pref_key_preview_show_up_duration"; public static final String PREF_KEY_PREVIEW_DISMISS_DURATION = diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java index dc2f88aa8..5640e2039 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java @@ -78,12 +78,18 @@ public final class DebugSettingsFragment extends SubScreenFragment res.getInteger(R.integer.config_key_preview_show_up_duration)); setupKeyPreviewAnimationDuration(DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION, res.getInteger(R.integer.config_key_preview_dismiss_duration)); - setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_SCALE, - ResourceUtils.getFloatFromFraction( - res, R.fraction.config_key_preview_show_up_start_scale)); - setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_SCALE, - ResourceUtils.getFloatFromFraction( - res, R.fraction.config_key_preview_dismiss_end_scale)); + final float defaultKeyPreviewShowUpStartScale = ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_show_up_start_scale); + final float defaultKeyPreviewDismissEndScale = ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_dismiss_end_scale); + setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE, + defaultKeyPreviewShowUpStartScale); + setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE, + defaultKeyPreviewShowUpStartScale); + setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_X_SCALE, + defaultKeyPreviewDismissEndScale); + setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE, + defaultKeyPreviewDismissEndScale); mServiceNeedsRestart = false; mDebugMode = (TwoStatePreference) findPreference(DebugSettings.PREF_DEBUG_MODE); diff --git a/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java index fcdd39316..b073c50a4 100644 --- a/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java @@ -16,66 +16,27 @@ package com.android.inputmethod.latin.settings; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Resources; import android.os.Bundle; -import android.preference.PreferenceScreen; -import android.text.TextUtils; -import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; -import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.util.ArrayList; /** - * "Multi lingual options" settings sub screen. + * "Multilingual options" settings sub screen. * * This settings sub screen handles the following input preferences. * - Language switch key * - Switch to other input methods - * - Custom input styles */ public final class MultiLingualSettingsFragment extends SubScreenFragment { @Override public void onCreate(final Bundle icicle) { super.onCreate(icicle); - addPreferencesFromResource(R.xml.prefs_screen_multi_lingual); - - 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()}. - SubtypeLocaleUtils.init(context); - + addPreferencesFromResource(R.xml.prefs_screen_multilingual); if (!Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS) { removePreference(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY); removePreference(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST); } } - - @Override - public void onResume() { - super.onResume(); - updateCustomInputStylesSummary(); - } - - private void updateCustomInputStylesSummary() { - final SharedPreferences prefs = getSharedPreferences(); - final Resources res = getResources(); - final PreferenceScreen customInputStyles = - (PreferenceScreen)findPreference(Settings.PREF_CUSTOM_INPUT_STYLES); - final String prefSubtype = Settings.readPrefAdditionalSubtypes(prefs, res); - final InputMethodSubtype[] subtypes = - AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype); - final ArrayList<String> subtypeNames = new ArrayList<>(); - for (final InputMethodSubtype subtype : subtypes) { - subtypeNames.add(SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)); - } - // TODO: A delimiter of custom input styles should be localized. - customInputStyles.setSummary(TextUtils.join(", ", subtypeNames)); - } } diff --git a/java/src/com/android/inputmethod/latin/settings/InputSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java index f459d68dd..49db2bdc0 100644 --- a/java/src/com/android/inputmethod/latin/settings/InputSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java @@ -27,7 +27,7 @@ import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SubtypeSwitcher; /** - * "Input preferences" settings sub screen. + * "Preferences" settings sub screen. * * This settings sub screen handles the following input preferences. * - Auto-capitalization @@ -37,11 +37,11 @@ import com.android.inputmethod.latin.SubtypeSwitcher; * - Popup on keypress * - Voice input key */ -public final class InputSettingsFragment extends SubScreenFragment { +public final class PreferencesSettingsFragment extends SubScreenFragment { @Override public void onCreate(final Bundle icicle) { super.onCreate(icicle); - addPreferencesFromResource(R.xml.prefs_screen_input); + addPreferencesFromResource(R.xml.prefs_screen_preferences); final Resources res = getResources(); final Context context = getActivity(); diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 9d3c27bbe..0de2d8831 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -19,6 +19,7 @@ package com.android.inputmethod.latin.settings; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; import android.preference.PreferenceManager; @@ -41,9 +42,10 @@ import java.util.concurrent.locks.ReentrantLock; public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = Settings.class.getSimpleName(); // Settings screens - public static final String SCREEN_INPUT = "screen_input"; + public static final String SCREEN_PREFERENCES = "screen_preferences"; + public static final String SCREEN_APPEARANCE = "screen_appearance"; public static final String SCREEN_THEME = "screen_theme"; - public static final String SCREEN_MULTI_LINGUAL = "screen_multi_lingual"; + public static final String SCREEN_MULTILINGUAL = "screen_multilingual"; public static final String SCREEN_GESTURE = "screen_gesture"; public static final String SCREEN_CORRECTION = "screen_correction"; public static final String SCREEN_ADVANCED = "screen_advanced"; @@ -68,6 +70,9 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang "pref_key_use_double_space_period"; public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE = "pref_key_block_potentially_offensive"; + // No multilingual options in Android L and above for now. + public static final boolean SHOW_MULTILINGUAL_SETTINGS = + BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT; public static final boolean ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS = BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT; public static final boolean HAS_UI_TO_ACCEPT_TYPED_WORD = @@ -368,6 +373,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return prefs.getBoolean(PREF_SHOW_SETUP_WIZARD_ICON, false); } + public static boolean readHasHardwareKeyboard(final Configuration conf) { + // The standard way of finding out whether we have a hardware keyboard. This code is taken + // from InputMethodService#onEvaluateInputShown, which canonically determines this. + // In a nutshell, we have a keyboard if the configuration says the type of hardware keyboard + // is NOKEYS and if it's not hidden (e.g. folded inside the device). + return conf.keyboard != Configuration.KEYBOARD_NOKEYS + && conf.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES; + } + public static boolean isInternal(final SharedPreferences prefs) { return prefs.getBoolean(PREF_KEY_IS_INTERNAL, false); } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java index ff7495853..4fc17387f 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin.settings; import android.content.Intent; import android.os.Bundle; +import android.preference.Preference; import android.preference.PreferenceScreen; import android.view.Menu; import android.view.MenuInflater; @@ -46,12 +47,10 @@ public final class SettingsFragment extends InputMethodSettingsFragment { final PreferenceScreen preferenceScreen = getPreferenceScreen(); preferenceScreen.setTitle( ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class)); - } - - @Override - public void onResume() { - super.onResume(); - ThemeSettingsFragment.updateKeyboardThemeSummary(findPreference(Settings.SCREEN_THEME)); + if (!Settings.SHOW_MULTILINGUAL_SETTINGS) { + final Preference multilingualOptions = findPreference(Settings.SCREEN_MULTILINGUAL); + preferenceScreen.removePreference(multilingualOptions); + } } @Override diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index 1cd7b391a..d8c548d8b 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -52,7 +52,10 @@ public final class SettingsValues { public final SpacingAndPunctuations mSpacingAndPunctuations; public final int mDelayInMillisecondsToUpdateOldSuggestions; public final long mDoubleSpacePeriodTimeout; - + // From configuration: + public final Locale mLocale; + public final boolean mHasHardwareKeyboard; + public final int mDisplayOrientation; // From preferences, in the same order as xml/prefs.xml: public final boolean mAutoCap; public final boolean mVibrateOn; @@ -73,7 +76,6 @@ public final class SettingsValues { public final boolean mSlidingKeyInputPreviewEnabled; public final boolean mPhraseGestureEnabled; public final int mKeyLongpressTimeout; - public final Locale mLocale; public final boolean mEnableMetricsLogging; public final boolean mShouldShowUiToAcceptTypedWord; @@ -88,19 +90,24 @@ public final class SettingsValues { public final float mAutoCorrectionThreshold; public final boolean mAutoCorrectionEnabledPerUserSettings; private final boolean mSuggestionsEnabledPerUserSettings; - public final int mDisplayOrientation; private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds; // Setting values for additional features public final int[] mAdditionalFeaturesSettingValues = new int[AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE]; + // TextDecorator + public final int mTextHighlightColorForAddToDictionaryIndicator; + // Debug settings public final boolean mIsInternal; + public final boolean mHasCustomKeyPreviewAnimationParams; public final int mKeyPreviewShowUpDuration; public final int mKeyPreviewDismissDuration; - public final float mKeyPreviewShowUpStartScale; - public final float mKeyPreviewDismissEndScale; + public final float mKeyPreviewShowUpStartXScale; + public final float mKeyPreviewShowUpStartYScale; + public final float mKeyPreviewDismissEndXScale; + public final float mKeyPreviewDismissEndYScale; public SettingsValues(final Context context, final SharedPreferences prefs, final Resources res, final InputAttributes inputAttributes) { @@ -138,11 +145,13 @@ public final class SettingsValues { ? Settings.readShowsLanguageSwitchKey(prefs) : true /* forcibly */; mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true); mUsePersonalizedDicts = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true); - mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true); + mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true) + && inputAttributes.mIsGeneralTextInput; mBlockPotentiallyOffensive = Settings.readBlockPotentiallyOffensive(prefs, res); mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(autoCorrectionThresholdRawValue, res); mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res); mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout); + mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration()); mEnableMetricsLogging = prefs.getBoolean(Settings.PREF_ENABLE_METRICS_LOGGING, true); mShouldShowUiToAcceptTypedWord = Settings.HAS_UI_TO_ACCEPT_TYPED_WORD && prefs.getBoolean(DebugSettings.PREF_SHOW_UI_TO_ACCEPT_TYPED_WORD, true); @@ -163,21 +172,33 @@ public final class SettingsValues { mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs); AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray( prefs, mAdditionalFeaturesSettingValues); + mTextHighlightColorForAddToDictionaryIndicator = res.getColor( + R.color.text_decorator_add_to_dictionary_indicator_text_highlight_color); mIsInternal = Settings.isInternal(prefs); + mHasCustomKeyPreviewAnimationParams = prefs.getBoolean( + DebugSettings.PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS, false); mKeyPreviewShowUpDuration = Settings.readKeyPreviewAnimationDuration( prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION, res.getInteger(R.integer.config_key_preview_show_up_duration)); mKeyPreviewDismissDuration = Settings.readKeyPreviewAnimationDuration( prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION, res.getInteger(R.integer.config_key_preview_dismiss_duration)); - mKeyPreviewShowUpStartScale = Settings.readKeyPreviewAnimationScale( - prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_SCALE, - ResourceUtils.getFloatFromFraction( - res, R.fraction.config_key_preview_show_up_start_scale)); - mKeyPreviewDismissEndScale = Settings.readKeyPreviewAnimationScale( - prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_SCALE, - ResourceUtils.getFloatFromFraction( - res, R.fraction.config_key_preview_dismiss_end_scale)); + final float defaultKeyPreviewShowUpStartScale = ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_show_up_start_scale); + final float defaultKeyPreviewDismissEndScale = ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_dismiss_end_scale); + mKeyPreviewShowUpStartXScale = Settings.readKeyPreviewAnimationScale( + prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE, + defaultKeyPreviewShowUpStartScale); + mKeyPreviewShowUpStartYScale = Settings.readKeyPreviewAnimationScale( + prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE, + defaultKeyPreviewShowUpStartScale); + mKeyPreviewDismissEndXScale = Settings.readKeyPreviewAnimationScale( + prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_X_SCALE, + defaultKeyPreviewDismissEndScale); + mKeyPreviewDismissEndYScale = Settings.readKeyPreviewAnimationScale( + prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE, + defaultKeyPreviewDismissEndScale); mDisplayOrientation = res.getConfiguration().orientation; mAppWorkarounds = new AsyncResultHolder<>(); final PackageInfo packageInfo = TargetPackageInfoGetterTask.getCachedPackageInfo( @@ -396,16 +417,22 @@ public final class SettingsValues { sb.append("" + (null == awu ? "null" : awu.toString())); sb.append("\n mAdditionalFeaturesSettingValues = "); sb.append("" + Arrays.toString(mAdditionalFeaturesSettingValues)); + sb.append("\n mTextHighlightColorForAddToDictionaryIndicator = "); + sb.append("" + mTextHighlightColorForAddToDictionaryIndicator); sb.append("\n mIsInternal = "); sb.append("" + mIsInternal); sb.append("\n mKeyPreviewShowUpDuration = "); sb.append("" + mKeyPreviewShowUpDuration); sb.append("\n mKeyPreviewDismissDuration = "); sb.append("" + mKeyPreviewDismissDuration); - sb.append("\n mKeyPreviewShowUpStartScale = "); - sb.append("" + mKeyPreviewShowUpStartScale); - sb.append("\n mKeyPreviewDismissEndScale = "); - sb.append("" + mKeyPreviewDismissEndScale); + sb.append("\n mKeyPreviewShowUpStartScaleX = "); + sb.append("" + mKeyPreviewShowUpStartXScale); + sb.append("\n mKeyPreviewShowUpStartScaleY = "); + sb.append("" + mKeyPreviewShowUpStartYScale); + sb.append("\n mKeyPreviewDismissEndScaleX = "); + sb.append("" + mKeyPreviewDismissEndXScale); + sb.append("\n mKeyPreviewDismissEndScaleY = "); + sb.append("" + mKeyPreviewDismissEndYScale); return sb.toString(); } } diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java index b8d2a2248..49d81104d 100644 --- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java +++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin.settings; import android.content.res.Resources; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.internal.MoreKeySpec; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.PunctuationSuggestions; @@ -68,6 +69,22 @@ public final class SpacingAndPunctuations { mSuggestPuncList = PunctuationSuggestions.newPunctuationSuggestions(suggestPuncsSpec); } + @UsedForTesting + public SpacingAndPunctuations(final SpacingAndPunctuations model, + final int[] overrideSortedWordSeparators) { + mSortedSymbolsPrecededBySpace = model.mSortedSymbolsPrecededBySpace; + mSortedSymbolsFollowedBySpace = model.mSortedSymbolsFollowedBySpace; + mSortedSymbolsClusteringTogether = model.mSortedSymbolsClusteringTogether; + mSortedWordConnectors = model.mSortedWordConnectors; + mSortedWordSeparators = overrideSortedWordSeparators; + mSuggestPuncList = model.mSuggestPuncList; + mSentenceSeparator = model.mSentenceSeparator; + mSentenceSeparatorAndSpace = model.mSentenceSeparatorAndSpace; + mCurrentLanguageHasSpaces = model.mCurrentLanguageHasSpaces; + mUsesAmericanTypography = model.mUsesAmericanTypography; + mUsesGermanRules = model.mUsesGermanRules; + } + public boolean isWordSeparator(final int code) { return Arrays.binarySearch(mSortedWordSeparators, code) >= 0; } diff --git a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java index 9585736e7..3f0b10225 100644 --- a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java +++ b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java @@ -24,7 +24,6 @@ import android.content.pm.PackageManager; import android.preference.PreferenceManager; import android.util.Log; -import com.android.inputmethod.compat.IntentCompatUtils; import com.android.inputmethod.latin.settings.Settings; /** @@ -55,14 +54,6 @@ import com.android.inputmethod.latin.settings.Settings; public final class LauncherIconVisibilityManager { private static final String TAG = LauncherIconVisibilityManager.class.getSimpleName(); - public static void onReceiveGlobalIntent(final String action, final Context context) { - if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(action) || - Intent.ACTION_BOOT_COMPLETED.equals(action) || - IntentCompatUtils.is_ACTION_USER_INITIALIZE(action)) { - updateSetupWizardIconVisibility(context); - } - } - public static void updateSetupWizardIconVisibility(final Context context) { final ComponentName setupWizardActivity = new ComponentName(context, SetupActivity.class); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index 33745a846..0fd5e139e 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -408,8 +408,8 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick // Decided to be in the sliding suggestion mode only when the touch point has been moved // upward. Further {@link MotionEvent}s will be delivered to // {@link #onTouchEvent(MotionEvent)}. - mNeedsToTransformTouchEventToHoverEvent = AccessibilityUtils.getInstance() - .isTouchExplorationEnabled(); + mNeedsToTransformTouchEventToHoverEvent = + AccessibilityUtils.getInstance().isTouchExplorationEnabled(); mIsDispatchingHoverEventToMoreSuggestions = false; return true; } diff --git a/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java new file mode 100644 index 000000000..9dc0524a2 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java @@ -0,0 +1,236 @@ +/* + * 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.utils; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.inputmethodservice.ExtractEditText; +import android.inputmethodservice.InputMethodService; +import android.text.Layout; +import android.text.Spannable; +import android.view.View; +import android.view.ViewParent; +import android.view.inputmethod.CursorAnchorInfo; +import android.widget.TextView; + +/** + * This class allows input methods to extract {@link CursorAnchorInfo} directly from the given + * {@link TextView}. This is useful and even necessary to support full-screen mode where the default + * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} event callback must be + * ignored because it reports the character locations of the target application rather than + * characters on {@link ExtractEditText}. + */ +public final class CursorAnchorInfoUtils { + private CursorAnchorInfoUtils() { + // This helper class is not instantiable. + } + + private static boolean isPositionVisible(final View view, final float positionX, + final float positionY) { + final float[] position = new float[] { positionX, positionY }; + View currentView = view; + + while (currentView != null) { + if (currentView != view) { + // Local scroll is already taken into account in positionX/Y + position[0] -= currentView.getScrollX(); + position[1] -= currentView.getScrollY(); + } + + if (position[0] < 0 || position[1] < 0 || + position[0] > currentView.getWidth() || position[1] > currentView.getHeight()) { + return false; + } + + if (!currentView.getMatrix().isIdentity()) { + currentView.getMatrix().mapPoints(position); + } + + position[0] += currentView.getLeft(); + position[1] += currentView.getTop(); + + final ViewParent parent = currentView.getParent(); + if (parent instanceof View) { + currentView = (View) parent; + } else { + // We've reached the ViewRoot, stop iterating + currentView = null; + } + } + + // We've been able to walk up the view hierarchy and the position was never clipped + return true; + } + + /** + * Returns {@link CursorAnchorInfo} from the given {@link TextView}. + * @param textView the target text view from which {@link CursorAnchorInfo} is to be extracted. + * @return the {@link CursorAnchorInfo} object based on the current layout. {@code null} if it + * is not feasible. + */ + public static CursorAnchorInfo getCursorAnchorInfo(final TextView textView) { + Layout layout = textView.getLayout(); + if (layout == null) { + return null; + } + + final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder(); + + final int selectionStart = textView.getSelectionStart(); + builder.setSelectionRange(selectionStart, textView.getSelectionEnd()); + + // Construct transformation matrix from view local coordinates to screen coordinates. + final Matrix viewToScreenMatrix = new Matrix(textView.getMatrix()); + final int[] viewOriginInScreen = new int[2]; + textView.getLocationOnScreen(viewOriginInScreen); + viewToScreenMatrix.postTranslate(viewOriginInScreen[0], viewOriginInScreen[1]); + builder.setMatrix(viewToScreenMatrix); + + if (layout.getLineCount() == 0) { + return null; + } + final Rect lineBoundsWithoutOffset = new Rect(); + final Rect lineBoundsWithOffset = new Rect(); + layout.getLineBounds(0, lineBoundsWithoutOffset); + textView.getLineBounds(0, lineBoundsWithOffset); + final float viewportToContentHorizontalOffset = lineBoundsWithOffset.left + - lineBoundsWithoutOffset.left - textView.getScrollX(); + final float viewportToContentVerticalOffset = lineBoundsWithOffset.top + - lineBoundsWithoutOffset.top - textView.getScrollY(); + + final CharSequence text = textView.getText(); + if (text instanceof Spannable) { + // Here we assume that the composing text is marked as SPAN_COMPOSING flag. This is not + // necessarily true, but basically works. + int composingTextStart = text.length(); + int composingTextEnd = 0; + final Spannable spannable = (Spannable) text; + final Object[] spans = spannable.getSpans(0, text.length(), Object.class); + for (Object span : spans) { + final int spanFlag = spannable.getSpanFlags(span); + if ((spanFlag & Spannable.SPAN_COMPOSING) != 0) { + composingTextStart = Math.min(composingTextStart, + spannable.getSpanStart(span)); + composingTextEnd = Math.max(composingTextEnd, spannable.getSpanEnd(span)); + } + } + + final boolean hasComposingText = + (0 <= composingTextStart) && (composingTextStart < composingTextEnd); + if (hasComposingText) { + final CharSequence composingText = text.subSequence(composingTextStart, + composingTextEnd); + builder.setComposingText(composingTextStart, composingText); + + final int minLine = layout.getLineForOffset(composingTextStart); + final int maxLine = layout.getLineForOffset(composingTextEnd - 1); + for (int line = minLine; line <= maxLine; ++line) { + final int lineStart = layout.getLineStart(line); + final int lineEnd = layout.getLineEnd(line); + final int offsetStart = Math.max(lineStart, composingTextStart); + final int offsetEnd = Math.min(lineEnd, composingTextEnd); + final boolean ltrLine = + layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT; + final float[] widths = new float[offsetEnd - offsetStart]; + layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths); + final float top = layout.getLineTop(line); + final float bottom = layout.getLineBottom(line); + for (int offset = offsetStart; offset < offsetEnd; ++offset) { + final float charWidth = widths[offset - offsetStart]; + final boolean isRtl = layout.isRtlCharAt(offset); + final float primary = layout.getPrimaryHorizontal(offset); + final float secondary = layout.getSecondaryHorizontal(offset); + // TODO: This doesn't work perfectly for text with custom styles and TAB + // chars. + final float left; + final float right; + if (ltrLine) { + if (isRtl) { + left = secondary - charWidth; + right = secondary; + } else { + left = primary; + right = primary + charWidth; + } + } else { + if (!isRtl) { + left = secondary; + right = secondary + charWidth; + } else { + left = primary - charWidth; + right = primary; + } + } + // TODO: Check top-right and bottom-left as well. + final float localLeft = left + viewportToContentHorizontalOffset; + final float localRight = right + viewportToContentHorizontalOffset; + final float localTop = top + viewportToContentVerticalOffset; + final float localBottom = bottom + viewportToContentVerticalOffset; + final boolean isTopLeftVisible = isPositionVisible(textView, + localLeft, localTop); + final boolean isBottomRightVisible = + isPositionVisible(textView, localRight, localBottom); + int characterBoundsFlags = 0; + if (isTopLeftVisible || isBottomRightVisible) { + characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; + } + if (!isTopLeftVisible || !isTopLeftVisible) { + characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; + } + if (isRtl) { + characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL; + } + // Here offset is the index in Java chars. + builder.addCharacterBounds(offset, localLeft, localTop, localRight, + localBottom, characterBoundsFlags); + } + } + } + } + + // Treat selectionStart as the insertion point. + if (0 <= selectionStart) { + final int offset = selectionStart; + final int line = layout.getLineForOffset(offset); + final float insertionMarkerX = layout.getPrimaryHorizontal(offset) + + viewportToContentHorizontalOffset; + final float insertionMarkerTop = layout.getLineTop(line) + + viewportToContentVerticalOffset; + final float insertionMarkerBaseline = layout.getLineBaseline(line) + + viewportToContentVerticalOffset; + final float insertionMarkerBottom = layout.getLineBottom(line) + + viewportToContentVerticalOffset; + final boolean isTopVisible = + isPositionVisible(textView, insertionMarkerX, insertionMarkerTop); + final boolean isBottomVisible = + isPositionVisible(textView, insertionMarkerX, insertionMarkerBottom); + int insertionMarkerFlags = 0; + if (isTopVisible || isBottomVisible) { + insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; + } + if (!isTopVisible || !isBottomVisible) { + insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; + } + if (layout.isRtlCharAt(offset)) { + insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL; + } + builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop, + insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags); + } + return builder.build(); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java index 08f5b0b41..c2167a76b 100644 --- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java @@ -19,12 +19,13 @@ 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.AdvancedSettingsFragment; +import com.android.inputmethod.latin.settings.AppearanceSettingsFragment; import com.android.inputmethod.latin.settings.CorrectionSettingsFragment; import com.android.inputmethod.latin.settings.CustomInputStyleSettingsFragment; import com.android.inputmethod.latin.settings.DebugSettingsFragment; import com.android.inputmethod.latin.settings.GestureSettingsFragment; -import com.android.inputmethod.latin.settings.InputSettingsFragment; import com.android.inputmethod.latin.settings.MultiLingualSettingsFragment; +import com.android.inputmethod.latin.settings.PreferencesSettingsFragment; import com.android.inputmethod.latin.settings.SettingsFragment; import com.android.inputmethod.latin.settings.ThemeSettingsFragment; import com.android.inputmethod.latin.spellcheck.SpellCheckerSettingsFragment; @@ -40,7 +41,8 @@ public class FragmentUtils { static { sLatinImeFragments.add(DictionarySettingsFragment.class.getName()); sLatinImeFragments.add(AboutPreferences.class.getName()); - sLatinImeFragments.add(InputSettingsFragment.class.getName()); + sLatinImeFragments.add(PreferencesSettingsFragment.class.getName()); + sLatinImeFragments.add(AppearanceSettingsFragment.class.getName()); sLatinImeFragments.add(ThemeSettingsFragment.class.getName()); sLatinImeFragments.add(MultiLingualSettingsFragment.class.getName()); sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName()); diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java index 8b7077879..ea406fa75 100644 --- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java @@ -23,15 +23,24 @@ import android.provider.Settings.SettingNotFoundException; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.R; +import java.util.concurrent.TimeUnit; + public final class ImportantNoticeUtils { private static final String TAG = ImportantNoticeUtils.class.getSimpleName(); // {@link SharedPreferences} name to save the last important notice version that has been // displayed to users. private static final String PREFERENCE_NAME = "important_notice_pref"; - private static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version"; + @UsedForTesting + static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version"; + @UsedForTesting + static final String KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE = + "timestamp_of_first_important_notice"; + @UsedForTesting + static final long TIMEOUT_OF_IMPORTANT_NOTICE = TimeUnit.HOURS.toMillis(23); public static final int VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS = 1; // Copy of the hidden {@link Settings.Secure#USER_SETUP_COMPLETE} settings key. @@ -56,15 +65,18 @@ public final class ImportantNoticeUtils { } } - private static SharedPreferences getImportantNoticePreferences(final Context context) { + @UsedForTesting + static SharedPreferences getImportantNoticePreferences(final Context context) { return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); } - private static int getCurrentImportantNoticeVersion(final Context context) { + @UsedForTesting + static int getCurrentImportantNoticeVersion(final Context context) { return context.getResources().getInteger(R.integer.config_important_notice_version); } - private static int getLastImportantNoticeVersion(final Context context) { + @UsedForTesting + static int getLastImportantNoticeVersion(final Context context) { return getImportantNoticePreferences(context).getInt(KEY_IMPORTANT_NOTICE_VERSION, 0); } @@ -77,6 +89,20 @@ public final class ImportantNoticeUtils { return getCurrentImportantNoticeVersion(context) > lastVersion; } + @UsedForTesting + static boolean hasTimeoutPassed(final Context context, final long currentTimeInMillis) { + final SharedPreferences prefs = getImportantNoticePreferences(context); + if (!prefs.contains(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)) { + prefs.edit() + .putLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, currentTimeInMillis) + .apply(); + } + final long firstDisplayTimeInMillis = prefs.getLong( + KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, currentTimeInMillis); + final long elapsedTime = currentTimeInMillis - firstDisplayTimeInMillis; + return elapsedTime >= TIMEOUT_OF_IMPORTANT_NOTICE; + } + public static boolean shouldShowImportantNotice(final Context context) { if (!hasNewImportantNotice(context)) { return false; @@ -88,6 +114,10 @@ public final class ImportantNoticeUtils { if (isInSystemSetupWizard(context)) { return false; } + if (hasTimeoutPassed(context, System.currentTimeMillis())) { + updateLastImportantNoticeVersion(context); + return false; + } return true; } @@ -95,11 +125,12 @@ public final class ImportantNoticeUtils { getImportantNoticePreferences(context) .edit() .putInt(KEY_IMPORTANT_NOTICE_VERSION, getNextImportantNoticeVersion(context)) + .remove(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE) .apply(); } public static String getNextImportantNoticeTitle(final Context context) { - final int nextVersion = getCurrentImportantNoticeVersion(context); + final int nextVersion = getNextImportantNoticeVersion(context); final String[] importantNoticeTitleArray = context.getResources().getStringArray( R.array.important_notice_title_array); if (nextVersion > 0 && nextVersion < importantNoticeTitleArray.length) { diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index 38f0b3fee..79128dbd2 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. } @@ -123,20 +131,20 @@ public final class StringUtils { public static String capitalizeFirstCodePoint(final String s, final Locale locale) { if (s.length() <= 1) { - return s.toUpperCase(locale); + return toUpperCaseOfStringForLocale(s, true /* needsToUpperCase */, locale); } // Please refer to the comment below in // {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings final int cutoff = s.offsetByCodePoints(0, 1); - return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff); + return toUpperCaseOfStringForLocale( + s.substring(0, cutoff), true /* needsToUpperCase */, locale) + s.substring(cutoff); } public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) { if (s.length() <= 1) { - return s.toUpperCase(locale); + return toUpperCaseOfStringForLocale(s, true /* needsToUpperCase */, locale); } // TODO: fix the bugs below - // - This does not work for Greek, because it returns upper case instead of title case. // - It does not work for Serbian, because it fails to account for the "lj" character, // which should be "Lj" in title case and "LJ" in upper case. // - It does not work for Dutch, because it fails to account for the "ij" digraph when it's @@ -144,7 +152,9 @@ public final class StringUtils { // be capitalized as "IJ" as if they were a single letter in most words (not all). If the // unicode char for the ligature is used however, it works. final int cutoff = s.offsetByCodePoints(0, 1); - return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff).toLowerCase(locale); + final String titleCaseFirstLetter = toUpperCaseOfStringForLocale( + s.substring(0, cutoff), true /* needsToUpperCase */, locale); + return titleCaseFirstLetter + s.substring(cutoff).toLowerCase(locale); } private static final int[] EMPTY_CODEPOINTS = {}; @@ -481,10 +491,23 @@ public final class StringUtils { return bytes; } + private static final String LANGUAGE_GREEK = "el"; + + private static Locale getLocaleUsedForToTitleCase(final Locale locale) { + // In Greek locale {@link String#toUpperCase(Locale)} eliminates accents from its result. + // In order to get accented upper case letter, {@link Locale#ROOT} should be used. + if (LANGUAGE_GREEK.equals(locale.getLanguage())) { + return Locale.ROOT; + } + return locale; + } + public static String toUpperCaseOfStringForLocale(final String text, final boolean needsToUpperCase, final Locale locale) { - if (text == null || !needsToUpperCase) return text; - return text.toUpperCase(locale); + if (text == null || !needsToUpperCase) { + return text; + } + return text.toUpperCase(getLocaleUsedForToTitleCase(locale)); } public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, @@ -594,4 +617,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 7170bd789..8cd49509f 100644 --- a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java +++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java @@ -32,14 +32,18 @@ import java.util.TreeSet; public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { public final Locale mLocale; 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 Locale locale, final int capacity) { - this(locale, sSuggestedWordInfoComparator, capacity); + public SuggestionResults(final Locale locale, final int capacity, + final boolean isBeginningOfSentence) { + this(locale, sSuggestedWordInfoComparator, capacity, isBeginningOfSentence); } - public SuggestionResults(final Locale locale, final Comparator<SuggestedWordInfo> comparator, - final int capacity) { + private SuggestionResults(final Locale locale, final Comparator<SuggestedWordInfo> comparator, + final int capacity, final boolean isBeginningOfSentence) { super(comparator); mLocale = locale; mCapacity = capacity; @@ -48,6 +52,7 @@ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { } else { mRawSuggestions = null; } + mIsBeginningOfSentence = isBeginningOfSentence; } @Override diff --git a/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java index f9d853493..dd122b634 100644 --- a/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java @@ -19,7 +19,10 @@ package com.android.inputmethod.latin.utils; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; +import android.view.Window; +import android.view.WindowManager; import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.RelativeLayout; public final class ViewLayoutUtils { @@ -51,4 +54,40 @@ public final class ViewLayoutUtils { marginLayoutParams.setMargins(x, y, 0, 0); } } + + public static void updateLayoutHeightOf(final Window window, final int layoutHeight) { + final WindowManager.LayoutParams params = window.getAttributes(); + if (params.height != layoutHeight) { + params.height = layoutHeight; + window.setAttributes(params); + } + } + + public static void updateLayoutHeightOf(final View view, final int layoutHeight) { + final ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params.height != layoutHeight) { + params.height = layoutHeight; + view.setLayoutParams(params); + } + } + + public static void updateLayoutGravityOf(final View view, final int layoutGravity) { + final ViewGroup.LayoutParams lp = view.getLayoutParams(); + if (lp instanceof LinearLayout.LayoutParams) { + final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)lp; + if (params.gravity != layoutGravity) { + params.gravity = layoutGravity; + view.setLayoutParams(params); + } + } else if (lp instanceof FrameLayout.LayoutParams) { + final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)lp; + if (params.gravity != layoutGravity) { + params.gravity = layoutGravity; + view.setLayoutParams(params); + } + } else { + throw new IllegalArgumentException("Layout parameter doesn't have gravity: " + + lp.getClass().getName()); + } + } } |