• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.inputmethod.latin;
18 
19 import com.android.inputmethod.event.CombinerChain;
20 import com.android.inputmethod.event.Event;
21 import com.android.inputmethod.latin.define.DebugFlags;
22 import com.android.inputmethod.latin.utils.CoordinateUtils;
23 import com.android.inputmethod.latin.utils.StringUtils;
24 
25 import java.util.ArrayList;
26 import java.util.Collections;
27 
28 import javax.annotation.Nonnull;
29 
30 /**
31  * A place to store the currently composing word with information such as adjacent key codes as well
32  */
33 public final class WordComposer {
34     private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
35     private static final boolean DBG = DebugFlags.DEBUG_ENABLED;
36 
37     public static final int CAPS_MODE_OFF = 0;
38     // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
39     // aren't used anywhere in the code
40     public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
41     public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
42     public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
43     public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
44 
45     private CombinerChain mCombinerChain;
46     private String mCombiningSpec; // Memory so that we don't uselessly recreate the combiner chain
47 
48     // The list of events that served to compose this string.
49     private final ArrayList<Event> mEvents;
50     private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH);
51     private String mAutoCorrection;
52     private boolean mIsResumed;
53     private boolean mIsBatchMode;
54     // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user
55     // gestures a word, is displeased with the results and hits backspace, then gestures again.
56     // At the very least we should avoid re-suggesting the same thing, and to do that we memorize
57     // the rejected suggestion in this variable.
58     // TODO: this should be done in a comprehensive way by the User History feature instead of
59     // as an ad-hockery here.
60     private String mRejectedBatchModeSuggestion;
61 
62     // Cache these values for performance
63     private CharSequence mTypedWordCache;
64     private int mCapsCount;
65     private int mDigitsCount;
66     private int mCapitalizedMode;
67     // This is the number of code points entered so far. This is not limited to MAX_WORD_LENGTH.
68     // In general, this contains the size of mPrimaryKeyCodes, except when this is greater than
69     // MAX_WORD_LENGTH in which case mPrimaryKeyCodes only contain the first MAX_WORD_LENGTH
70     // code points.
71     private int mCodePointSize;
72     private int mCursorPositionWithinWord;
73 
74     /**
75      * Whether the composing word has the only first char capitalized.
76      */
77     private boolean mIsOnlyFirstCharCapitalized;
78 
WordComposer()79     public WordComposer() {
80         mCombinerChain = new CombinerChain("");
81         mEvents = new ArrayList<>();
82         mAutoCorrection = null;
83         mIsResumed = false;
84         mIsBatchMode = false;
85         mCursorPositionWithinWord = 0;
86         mRejectedBatchModeSuggestion = null;
87         refreshTypedWordCache();
88     }
89 
90     /**
91      * Restart the combiners, possibly with a new spec.
92      * @param combiningSpec The spec string for combining. This is found in the extra value.
93      */
restartCombining(final String combiningSpec)94     public void restartCombining(final String combiningSpec) {
95         final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec;
96         if (!nonNullCombiningSpec.equals(mCombiningSpec)) {
97             mCombinerChain = new CombinerChain(
98                     mCombinerChain.getComposingWordWithCombiningFeedback().toString(),
99                     CombinerChain.createCombiners(nonNullCombiningSpec));
100             mCombiningSpec = nonNullCombiningSpec;
101         }
102     }
103 
104     /**
105      * Clear out the keys registered so far.
106      */
reset()107     public void reset() {
108         mCombinerChain.reset();
109         mEvents.clear();
110         mAutoCorrection = null;
111         mCapsCount = 0;
112         mDigitsCount = 0;
113         mIsOnlyFirstCharCapitalized = false;
114         mIsResumed = false;
115         mIsBatchMode = false;
116         mCursorPositionWithinWord = 0;
117         mRejectedBatchModeSuggestion = null;
118         refreshTypedWordCache();
119     }
120 
refreshTypedWordCache()121     private final void refreshTypedWordCache() {
122         mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback();
123         mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length());
124     }
125 
126     /**
127      * Number of keystrokes in the composing word.
128      * @return the number of keystrokes
129      */
130     // This may be made public if need be, but right now it's not used anywhere
size()131     /* package for tests */ int size() {
132         return mCodePointSize;
133     }
134 
135     /**
136      * Copy the code points in the typed word to a destination array of ints.
137      *
138      * If the array is too small to hold the code points in the typed word, nothing is copied and
139      * -1 is returned.
140      *
141      * @param destination the array of ints.
142      * @return the number of copied code points.
143      */
copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount( final int[] destination)144     public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
145             final int[] destination) {
146         // This method can be called on a separate thread and mTypedWordCache can change while we
147         // are executing this method.
148         final String typedWord = mTypedWordCache.toString();
149         // lastIndex is exclusive
150         final int lastIndex = typedWord.length()
151                 - StringUtils.getTrailingSingleQuotesCount(typedWord);
152         if (lastIndex <= 0) {
153             // The string is empty or contains only single quotes.
154             return 0;
155         }
156 
157         // The following function counts the number of code points in the text range which begins
158         // at index 0 and extends to the character at lastIndex.
159         final int codePointSize = Character.codePointCount(typedWord, 0, lastIndex);
160         if (codePointSize > destination.length) {
161             return -1;
162         }
163         return StringUtils.copyCodePointsAndReturnCodePointCount(destination, typedWord, 0,
164                 lastIndex, true /* downCase */);
165     }
166 
isSingleLetter()167     public boolean isSingleLetter() {
168         return size() == 1;
169     }
170 
isComposingWord()171     public final boolean isComposingWord() {
172         return size() > 0;
173     }
174 
getInputPointers()175     public InputPointers getInputPointers() {
176         return mInputPointers;
177     }
178 
179     /**
180      * Process an event and return an event, and return a processed event to apply.
181      * @param event the unprocessed event.
182      * @return the processed event. Never null, but may be marked as consumed.
183      */
184     @Nonnull
processEvent(final Event event)185     public Event processEvent(final Event event) {
186         final Event processedEvent = mCombinerChain.processEvent(mEvents, event);
187         // The retained state of the combiner chain may have changed while processing the event,
188         // so we need to update our cache.
189         refreshTypedWordCache();
190         mEvents.add(event);
191         return processedEvent;
192     }
193 
194     /**
195      * Apply a processed input event.
196      *
197      * All input events should be supported, including software/hardware events, characters as well
198      * as deletions, multiple inputs and gestures.
199      *
200      * @param event the event to apply. Must not be null.
201      */
applyProcessedEvent(final Event event)202     public void applyProcessedEvent(final Event event) {
203         mCombinerChain.applyProcessedEvent(event);
204         final int primaryCode = event.mCodePoint;
205         final int keyX = event.mX;
206         final int keyY = event.mY;
207         final int newIndex = size();
208         refreshTypedWordCache();
209         mCursorPositionWithinWord = mCodePointSize;
210         // We may have deleted the last one.
211         if (0 == mCodePointSize) {
212             mIsOnlyFirstCharCapitalized = false;
213         }
214         if (Constants.CODE_DELETE != event.mKeyCode) {
215             if (newIndex < MAX_WORD_LENGTH) {
216                 // In the batch input mode, the {@code mInputPointers} holds batch input points and
217                 // shouldn't be overridden by the "typed key" coordinates
218                 // (See {@link #setBatchInputWord}).
219                 if (!mIsBatchMode) {
220                     // TODO: Set correct pointer id and time
221                     mInputPointers.addPointerAt(newIndex, keyX, keyY, 0, 0);
222                 }
223             }
224             if (0 == newIndex) {
225                 mIsOnlyFirstCharCapitalized = Character.isUpperCase(primaryCode);
226             } else {
227                 mIsOnlyFirstCharCapitalized = mIsOnlyFirstCharCapitalized
228                         && !Character.isUpperCase(primaryCode);
229             }
230             if (Character.isUpperCase(primaryCode)) mCapsCount++;
231             if (Character.isDigit(primaryCode)) mDigitsCount++;
232         }
233         mAutoCorrection = null;
234     }
235 
setCursorPositionWithinWord(final int posWithinWord)236     public void setCursorPositionWithinWord(final int posWithinWord) {
237         mCursorPositionWithinWord = posWithinWord;
238         // TODO: compute where that puts us inside the events
239     }
240 
isCursorFrontOrMiddleOfComposingWord()241     public boolean isCursorFrontOrMiddleOfComposingWord() {
242         if (DBG && mCursorPositionWithinWord > mCodePointSize) {
243             throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
244                     + "in a word of size " + mCodePointSize);
245         }
246         return mCursorPositionWithinWord != mCodePointSize;
247     }
248 
249     /**
250      * When the cursor is moved by the user, we need to update its position.
251      * If it falls inside the currently composing word, we don't reset the composition, and
252      * only update the cursor position.
253      *
254      * @param expectedMoveAmount How many java chars to move the cursor. Negative values move
255      * the cursor backward, positive values move the cursor forward.
256      * @return true if the cursor is still inside the composing word, false otherwise.
257      */
moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount)258     public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) {
259         // TODO: should uncommit the composing feedback
260         mCombinerChain.reset();
261         int actualMoveAmountWithinWord = 0;
262         int cursorPos = mCursorPositionWithinWord;
263         // TODO: Don't make that copy. We can do this directly from mTypedWordCache.
264         final int[] codePoints = StringUtils.toCodePointArray(mTypedWordCache);
265         if (expectedMoveAmount >= 0) {
266             // Moving the cursor forward for the expected amount or until the end of the word has
267             // been reached, whichever comes first.
268             while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) {
269                 actualMoveAmountWithinWord += Character.charCount(codePoints[cursorPos]);
270                 ++cursorPos;
271             }
272         } else {
273             // Moving the cursor backward for the expected amount or until the start of the word
274             // has been reached, whichever comes first.
275             while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) {
276                 --cursorPos;
277                 actualMoveAmountWithinWord -= Character.charCount(codePoints[cursorPos]);
278             }
279         }
280         // If the actual and expected amounts differ, we crossed the start or the end of the word
281         // so the result would not be inside the composing word.
282         if (actualMoveAmountWithinWord != expectedMoveAmount) return false;
283         mCursorPositionWithinWord = cursorPos;
284         return true;
285     }
286 
setBatchInputPointers(final InputPointers batchPointers)287     public void setBatchInputPointers(final InputPointers batchPointers) {
288         mInputPointers.set(batchPointers);
289         mIsBatchMode = true;
290     }
291 
setBatchInputWord(final String word)292     public void setBatchInputWord(final String word) {
293         reset();
294         mIsBatchMode = true;
295         final int length = word.length();
296         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
297             final int codePoint = Character.codePointAt(word, i);
298             // We don't want to override the batch input points that are held in mInputPointers
299             // (See {@link #add(int,int,int)}).
300             final Event processedEvent =
301                     processEvent(Event.createEventForCodePointFromUnknownSource(codePoint));
302             applyProcessedEvent(processedEvent);
303         }
304     }
305 
306     /**
307      * Set the currently composing word to the one passed as an argument.
308      * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
309      * @param codePoints the code points to set as the composing word.
310      * @param coordinates the x, y coordinates of the key in the CoordinateUtils format
311      */
setComposingWord(final int[] codePoints, final int[] coordinates)312     public void setComposingWord(final int[] codePoints, final int[] coordinates) {
313         reset();
314         final int length = codePoints.length;
315         for (int i = 0; i < length; ++i) {
316             final Event processedEvent =
317                     processEvent(Event.createEventForCodePointFromAlreadyTypedText(codePoints[i],
318                     CoordinateUtils.xFromArray(coordinates, i),
319                     CoordinateUtils.yFromArray(coordinates, i)));
320             applyProcessedEvent(processedEvent);
321         }
322         mIsResumed = true;
323     }
324 
325     /**
326      * Returns the word as it was typed, without any correction applied.
327      * @return the word that was typed so far. Never returns null.
328      */
getTypedWord()329     public String getTypedWord() {
330         return mTypedWordCache.toString();
331     }
332 
333     /**
334      * Whether this composer is composing or about to compose a word in which only the first letter
335      * is a capital.
336      *
337      * If we do have a composing word, we just return whether the word has indeed only its first
338      * character capitalized. If we don't, then we return a value based on the capitalized mode,
339      * which tell us what is likely to happen for the next composing word.
340      *
341      * @return capitalization preference
342      */
isOrWillBeOnlyFirstCharCapitalized()343     public boolean isOrWillBeOnlyFirstCharCapitalized() {
344         return isComposingWord() ? mIsOnlyFirstCharCapitalized
345                 : (CAPS_MODE_OFF != mCapitalizedMode);
346     }
347 
348     /**
349      * Whether or not all of the user typed chars are upper case
350      * @return true if all user typed chars are upper case, false otherwise
351      */
isAllUpperCase()352     public boolean isAllUpperCase() {
353         if (size() <= 1) {
354             return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
355                     || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
356         } else {
357             return mCapsCount == size();
358         }
359     }
360 
wasShiftedNoLock()361     public boolean wasShiftedNoLock() {
362         return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
363                 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
364     }
365 
366     /**
367      * Returns true if more than one character is upper case, otherwise returns false.
368      */
isMostlyCaps()369     public boolean isMostlyCaps() {
370         return mCapsCount > 1;
371     }
372 
373     /**
374      * Returns true if we have digits in the composing word.
375      */
hasDigits()376     public boolean hasDigits() {
377         return mDigitsCount > 0;
378     }
379 
380     /**
381      * Saves the caps mode at the start of composing.
382      *
383      * WordComposer needs to know about the caps mode for several reasons. The first is, we need
384      * to know after the fact what the reason was, to register the correct form into the user
385      * history dictionary: if the word was automatically capitalized, we should insert it in
386      * all-lower case but if it's a manual pressing of shift, then it should be inserted as is.
387      * Also, batch input needs to know about the current caps mode to display correctly
388      * capitalized suggestions.
389      * @param mode the mode at the time of start
390      */
setCapitalizedModeAtStartComposingTime(final int mode)391     public void setCapitalizedModeAtStartComposingTime(final int mode) {
392         mCapitalizedMode = mode;
393     }
394 
395     /**
396      * Before fetching suggestions, we don't necessarily know about the capitalized mode yet.
397      *
398      * If we don't have a composing word yet, we take a note of this mode so that we can then
399      * supply this information to the suggestion process. If we have a composing word, then
400      * the previous mode has priority over this.
401      * @param mode the mode just before fetching suggestions
402      */
adviseCapitalizedModeBeforeFetchingSuggestions(final int mode)403     public void adviseCapitalizedModeBeforeFetchingSuggestions(final int mode) {
404         if (!isComposingWord()) {
405             mCapitalizedMode = mode;
406         }
407     }
408 
409     /**
410      * Returns whether the word was automatically capitalized.
411      * @return whether the word was automatically capitalized
412      */
wasAutoCapitalized()413     public boolean wasAutoCapitalized() {
414         return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
415                 || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
416     }
417 
418     /**
419      * Sets the auto-correction for this word.
420      */
setAutoCorrection(final String correction)421     public void setAutoCorrection(final String correction) {
422         mAutoCorrection = correction;
423     }
424 
425     /**
426      * @return the auto-correction for this word, or null if none.
427      */
getAutoCorrectionOrNull()428     public String getAutoCorrectionOrNull() {
429         return mAutoCorrection;
430     }
431 
432     /**
433      * @return whether we started composing this word by resuming suggestion on an existing string
434      */
isResumed()435     public boolean isResumed() {
436         return mIsResumed;
437     }
438 
439     // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
440     // committedWord should contain suggestion spans if applicable.
commitWord(final int type, final CharSequence committedWord, final String separatorString, final PrevWordsInfo prevWordsInfo)441     public LastComposedWord commitWord(final int type, final CharSequence committedWord,
442             final String separatorString, final PrevWordsInfo prevWordsInfo) {
443         // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
444         // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
445         // the last composed word to ensure this does not happen.
446         final LastComposedWord lastComposedWord = new LastComposedWord(mEvents,
447                 mInputPointers, mTypedWordCache.toString(), committedWord, separatorString,
448                 prevWordsInfo, mCapitalizedMode);
449         mInputPointers.reset();
450         if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
451                 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
452             lastComposedWord.deactivate();
453         }
454         mCapsCount = 0;
455         mDigitsCount = 0;
456         mIsBatchMode = false;
457         mCombinerChain.reset();
458         mEvents.clear();
459         mCodePointSize = 0;
460         mIsOnlyFirstCharCapitalized = false;
461         mCapitalizedMode = CAPS_MODE_OFF;
462         refreshTypedWordCache();
463         mAutoCorrection = null;
464         mCursorPositionWithinWord = 0;
465         mIsResumed = false;
466         mRejectedBatchModeSuggestion = null;
467         return lastComposedWord;
468     }
469 
resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord)470     public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
471         mEvents.clear();
472         Collections.copy(mEvents, lastComposedWord.mEvents);
473         mInputPointers.set(lastComposedWord.mInputPointers);
474         mCombinerChain.reset();
475         refreshTypedWordCache();
476         mCapitalizedMode = lastComposedWord.mCapitalizedMode;
477         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
478         mCursorPositionWithinWord = mCodePointSize;
479         mRejectedBatchModeSuggestion = null;
480         mIsResumed = true;
481     }
482 
isBatchMode()483     public boolean isBatchMode() {
484         return mIsBatchMode;
485     }
486 
setRejectedBatchModeSuggestion(final String rejectedSuggestion)487     public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
488         mRejectedBatchModeSuggestion = rejectedSuggestion;
489     }
490 
getRejectedBatchModeSuggestion()491     public String getRejectedBatchModeSuggestion() {
492         return mRejectedBatchModeSuggestion;
493     }
494 }
495