diff options
Diffstat (limited to 'java/src')
17 files changed, 392 insertions, 315 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index 509068ac4..b1813a141 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -53,8 +53,6 @@ public final class KeySpecParser { private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; // Constants for parsing. - private static int COMMA = ','; - private static final char ESCAPE_CHAR = '\\'; private static final char LABEL_END = '|'; private static final String PREFIX_TEXT = "!text/"; static final String PREFIX_ICON = "!icon/"; @@ -80,14 +78,14 @@ public final class KeySpecParser { } private static String parseEscape(final String text) { - if (text.indexOf(ESCAPE_CHAR) < 0) { + if (text.indexOf(Constants.CSV_ESCAPE) < 0) { return text; } final int length = text.length(); final StringBuilder sb = new StringBuilder(); for (int pos = 0; pos < length; pos++) { final char c = text.charAt(pos); - if (c == ESCAPE_CHAR && pos + 1 < length) { + if (c == Constants.CSV_ESCAPE && pos + 1 < length) { // Skip escape char pos++; sb.append(text.charAt(pos)); @@ -99,7 +97,7 @@ public final class KeySpecParser { } private static int indexOfLabelEnd(final String moreKeySpec, final int start) { - if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) { + if (moreKeySpec.indexOf(Constants.CSV_ESCAPE, start) < 0) { final int end = moreKeySpec.indexOf(LABEL_END, start); if (end == 0) { throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec); @@ -109,7 +107,7 @@ public final class KeySpecParser { final int length = moreKeySpec.length(); for (int pos = start; pos < length; pos++) { final char c = moreKeySpec.charAt(pos); - if (c == ESCAPE_CHAR && pos + 1 < length) { + if (c == Constants.CSV_ESCAPE && pos + 1 < length) { // Skip escape char pos++; } else if (c == LABEL_END) { @@ -353,7 +351,7 @@ public final class KeySpecParser { final String name = text.substring(pos + prefixLen, end); sb.append(textsSet.getText(name)); pos = end - 1; - } else if (c == ESCAPE_CHAR) { + } else if (c == Constants.CSV_ESCAPE) { if (sb != null) { // Append both escape character and escaped character. sb.append(text.substring(pos, Math.min(pos + 2, size))); @@ -385,45 +383,6 @@ public final class KeySpecParser { return size; } - public static String[] parseCsvString(final String rawText, final KeyboardTextsSet textsSet) { - final String text = resolveTextReference(rawText, textsSet); - final int size = text.length(); - if (size == 0) { - return null; - } - if (StringUtils.codePointCount(text) == 1) { - return text.codePointAt(0) == COMMA ? null : new String[] { text }; - } - - ArrayList<String> list = null; - int start = 0; - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (c == COMMA) { - // Skip empty entry. - if (pos - start > 0) { - if (list == null) { - list = CollectionUtils.newArrayList(); - } - list.add(text.substring(start, pos)); - } - // Skip comma - start = pos + 1; - } else if (c == ESCAPE_CHAR) { - // Skip escape character and escaped character. - pos++; - } - } - final String remain = (size - start > 0) ? text.substring(start) : null; - if (list == null) { - return remain != null ? new String[] { remain } : null; - } - if (remain != null) { - list.add(remain); - } - return list.toArray(new String[list.size()]); - } - public static int getIntValue(final String[] moreKeys, final String key, final int defaultValue) { if (moreKeys == null) { diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java index fe75e20ae..5db3ebbd1 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java @@ -18,6 +18,8 @@ package com.android.inputmethod.keyboard.internal; import android.content.res.TypedArray; +import com.android.inputmethod.latin.StringUtils; + public abstract class KeyStyle { private final KeyboardTextsSet mTextsSet; @@ -39,7 +41,8 @@ public abstract class KeyStyle { protected String[] parseStringArray(final TypedArray a, final int index) { if (a.hasValue(index)) { - return KeySpecParser.parseCsvString(a.getString(index), mTextsSet); + final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); + return StringUtils.parseCsvString(text); } return null; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index e087a4565..ab851bd64 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -164,10 +164,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> { try { parseKeyboard(parser); } catch (XmlPullParserException e) { - Log.w(BUILDER_TAG, "keyboard XML parse error: " + e); + Log.w(BUILDER_TAG, "keyboard XML parse error", e); throw new IllegalArgumentException(e); } catch (IOException e) { - Log.w(BUILDER_TAG, "keyboard XML parse error: " + e); + Log.w(BUILDER_TAG, "keyboard XML parse error", e); throw new RuntimeException(e); } finally { parser.close(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java index 6ad9d286f..0ec6b0176 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java @@ -74,6 +74,7 @@ public final class KeyboardCodesSet { private static final int CODE_LEFT_CURLY_BRACKET = '{'; private static final int CODE_RIGHT_CURLY_BRACKET = '}'; + // This array should be aligned with the array RTL below. private static final int[] DEFAULT = { Constants.CODE_TAB, Constants.CODE_ENTER, @@ -117,6 +118,7 @@ public final class KeyboardCodesSet { DEFAULT[12], DEFAULT[13], DEFAULT[14], + DEFAULT[15], CODE_RIGHT_PARENTHESIS, CODE_LEFT_PARENTHESIS, CODE_GREATER_THAN_SIGN, @@ -140,6 +142,9 @@ public final class KeyboardCodesSet { }; static { + if (DEFAULT.length != RTL.length) { + throw new RuntimeException("Internal inconsistency"); + } for (int i = 0; i < ID_TO_NAME.length; i++) { sNameToIdMap.put(ID_TO_NAME[i], i); } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 4a8752e43..879f1db49 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -94,33 +95,54 @@ public final class BinaryDictionaryFileDumper { } /** + * Finds out whether the dictionary pack is available on this device. + * @param context A context to get the content resolver. + * @return whether the dictionary pack is present or not. + */ + private static boolean isDictionaryPackPresent(final Context context) { + final ContentResolver cr = context.getContentResolver(); + final ContentProviderClient client = + cr.acquireContentProviderClient(getProviderUriBuilder("").build()); + if (client != null) { + client.release(); + return true; + } else { + return false; + } + } + + /** * Queries a content provider for the list of word lists for a specific locale * available to copy into Latin IME. */ private static List<WordListInfo> getWordListWordListInfos(final Locale locale, final Context context, final boolean hasDefaultWordList) { - final ContentResolver resolver = context.getContentResolver(); final String clientId = context.getString(R.string.dictionary_pack_client_id); final Uri.Builder builder = getProviderUriBuilder(clientId); builder.appendPath(QUERY_PATH_DICT_INFO); builder.appendPath(locale.toString()); builder.appendQueryParameter(QUERY_PARAMETER_PROTOCOL, QUERY_PARAMETER_PROTOCOL_VALUE); if (!hasDefaultWordList) { - builder.appendQueryParameter(QUERY_PARAMETER_MAY_PROMPT_USER, QUERY_PARAMETER_TRUE); + builder.appendQueryParameter(QUERY_PARAMETER_MAY_PROMPT_USER, + QUERY_PARAMETER_TRUE); } final Uri dictionaryPackUri = builder.build(); - final Cursor c = resolver.query(dictionaryPackUri, DICTIONARY_PROJECTION, null, null, null); - if (null == c) { - reinitializeClientRecordInDictionaryContentProvider(context, resolver, clientId); - return Collections.<WordListInfo>emptyList(); - } - if (c.getCount() <= 0 || !c.moveToFirst()) { - c.close(); - return Collections.<WordListInfo>emptyList(); - } - + final ContentResolver resolver = context.getContentResolver(); try { + final Cursor c = resolver.query(dictionaryPackUri, DICTIONARY_PROJECTION, null, null, + null); + if (null == c) { + if (isDictionaryPackPresent(context)) { + reinitializeClientRecordInDictionaryContentProvider(context, resolver, + clientId); + } + return Collections.<WordListInfo>emptyList(); + } + if (c.getCount() <= 0 || !c.moveToFirst()) { + c.close(); + return Collections.<WordListInfo>emptyList(); + } final List<WordListInfo> list = CollectionUtils.newArrayList(); do { final String wordListId = c.getString(0); @@ -130,10 +152,20 @@ public final class BinaryDictionaryFileDumper { } while (c.moveToNext()); c.close(); return list; + } catch (IllegalArgumentException e) { + // Any method call on the content resolver may unexpectedly crash without notice + // if the content provider is not present (for example, while crypting a device). + // Testing seems to indicate that ContentResolver#query() merely returns null + // while ContentResolver#delete throws IllegalArgumentException but this is + // undocumented, so all ContentResolver methods should be protected. A crash here is + // dangerous because crashing here would brick any encrypted device - we need the + // keyboard to be up and working to enter the password. So let's be as safe as possible. + Log.e(TAG, "IllegalArgumentException: the dictionary pack can't be contacted?", e); + return Collections.<WordListInfo>emptyList(); } catch (Exception e) { // Just in case we hit a problem in communication with the dictionary pack. // We don't want to die. - Log.e(TAG, "Exception communicating with the dictionary pack : " + e); + Log.e(TAG, "Exception communicating with the dictionary pack", e); return Collections.<WordListInfo>emptyList(); } } @@ -248,7 +280,7 @@ public final class BinaryDictionaryFileDumper { return AssetFileAddress.makeFromFileName(finalFileName); } catch (Exception e) { if (DEBUG) { - Log.i(TAG, "Can't open word list in mode " + mode + " : " + e); + Log.i(TAG, "Can't open word list in mode " + mode, e); } if (null != outputFile) { // This may or may not fail. The file may not have been created if the @@ -266,12 +298,12 @@ public final class BinaryDictionaryFileDumper { if (null != decryptedStream) decryptedStream.close(); if (null != bufferedInputStream) bufferedInputStream.close(); } catch (Exception e) { - Log.e(TAG, "Exception while closing a file descriptor : " + e); + Log.e(TAG, "Exception while closing a file descriptor", e); } try { if (null != bufferedOutputStream) bufferedOutputStream.close(); } catch (Exception e) { - Log.e(TAG, "Exception while closing a file : " + e); + Log.e(TAG, "Exception while closing a file", e); } } } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 1cdc3b564..a96738b3e 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -210,7 +210,7 @@ final class BinaryDictionaryGetter { } } } catch (java.io.IOException e) { - Log.e(TAG, "IOException trying to cleanup files : " + e); + Log.e(TAG, "IOException trying to cleanup files", e); } } diff --git a/java/src/com/android/inputmethod/latin/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/CapsModeUtils.java new file mode 100644 index 000000000..1012cd519 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/CapsModeUtils.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.text.InputType; +import android.text.TextUtils; + +import java.util.Locale; + +public final class CapsModeUtils { + private CapsModeUtils() { + // This utility class is not publicly instantiable. + } + + /** + * Apply an auto-caps mode to a string. + * + * This intentionally does NOT apply manual caps mode. It only changes the capitalization if + * the mode is one of the auto-caps modes. + * @param s The string to capitalize. + * @param capitalizeMode The mode in which to capitalize. + * @param locale The locale for capitalizing. + * @return The capitalized string. + */ + public static String applyAutoCapsMode(final String s, final int capitalizeMode, + final Locale locale) { + if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) { + return s.toUpperCase(locale); + } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) { + return StringUtils.toTitleCase(s, locale); + } else { + return s; + } + } + + /** + * Return whether a constant represents an auto-caps mode (either auto-shift or auto-shift-lock) + * @param mode The mode to test for + * @return true if this represents an auto-caps mode, false otherwise + */ + public static boolean isAutoCapsMode(final int mode) { + return WordComposer.CAPS_MODE_AUTO_SHIFTED == mode + || WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == mode; + } + + /** + * Determine what caps mode should be in effect at the current offset in + * the text. Only the mode bits set in <var>reqModes</var> will be + * checked. Note that the caps mode flags here are explicitly defined + * to match those in {@link InputType}. + * + * This code is a straight copy of TextUtils.getCapsMode (modulo namespace and formatting + * issues). This will change in the future as we simplify the code for our use and fix bugs. + * + * @param cs The text that should be checked for caps modes. + * @param reqModes The modes to be checked: may be any combination of + * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and + * {@link TextUtils#CAP_MODE_SENTENCES}. + * @param locale The locale to consider for capitalization rules + * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs + * + * @return Returns the actual capitalization modes that can be in effect + * at the current position, which is any combination of + * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and + * {@link TextUtils#CAP_MODE_SENTENCES}. + */ + public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale, + final boolean hasSpaceBefore) { + // Quick description of what we want to do: + // CAP_MODE_CHARACTERS is always on. + // CAP_MODE_WORDS is on if there is some whitespace before the cursor. + // CAP_MODE_SENTENCES is on if there is some whitespace before the cursor, and the end + // of a sentence just before that. + // We ignore opening parentheses and the like just before the cursor for purposes of + // finding whitespace for WORDS and SENTENCES modes. + // The end of a sentence ends with a period, question mark or exclamation mark. If it's + // a period, it also needs not to be an abbreviation, which means it also needs to either + // be immediately preceded by punctuation, or by a string of only letters with single + // periods interleaved. + + // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on. + if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) { + // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already + // evaluated MODE_CHARACTERS, we can return. + return TextUtils.CAP_MODE_CHARACTERS & reqModes; + } + + // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes + // opening parentheses, brackets, opening quotes, everything that *opens* a span of + // text in the linguistic sense. In RTL languages, this is still an opening sign, although + // it may look like a right parenthesis for example. We also include double quote and + // single quote since they aren't start punctuation in the unicode sense, but should still + // be skipped for English. TODO: does this depend on the language? + int i; + if (hasSpaceBefore) { + i = cs.length() + 1; + } else { + for (i = cs.length(); i > 0; i--) { + final char c = cs.charAt(i - 1); + if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE + && Character.getType(c) != Character.START_PUNCTUATION) { + break; + } + } + } + + // We are now on the character that precedes any starting punctuation, so in the most + // frequent case this will be whitespace or a letter, although it may occasionally be a + // start of line, or some symbol. + + // Step 3 : Search for the start of a paragraph. From the starting point computed in step 2, + // we go back over any space or tab char sitting there. We find the start of a paragraph + // if the first char that's not a space or tab is a start of line (as in \n, start of text, + // or some other similar characters). + int j = i; + char prevChar = Constants.CODE_SPACE; + if (hasSpaceBefore) --j; + while (j > 0) { + prevChar = cs.charAt(j - 1); + if (!Character.isSpaceChar(prevChar) && prevChar != Constants.CODE_TAB) break; + j--; + } + if (j <= 0 || Character.isWhitespace(prevChar)) { + // There are only spacing chars between the start of the paragraph and the cursor, + // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both + // MODE_WORDS and MODE_SENTENCES should be active. + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS + | TextUtils.CAP_MODE_SENTENCES) & reqModes; + } + if (i == j) { + // If we don't have whitespace before index i, it means neither MODE_WORDS + // nor mode sentences should be on so we can return right away. + return TextUtils.CAP_MODE_CHARACTERS & reqModes; + } + if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) { + // Here we know we have whitespace before the cursor (if not, we returned in the above + // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate + // MODE_SENTENCES so we can return right away. + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + } + // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above, + // we know that MODE_SENTENCES is being requested. + + // Step 4 : Search for MODE_SENTENCES. + // English is a special case in that "American typography" rules, which are the most common + // in English, state that a sentence terminator immediately following a quotation mark + // should be swapped with it and de-duplicated (included in the quotation mark), + // e.g. <<Did he say, "let's go home?">> + // No other language has such a rule as far as I know, instead putting inside the quotation + // mark as the exact thing quoted and handling the surrounding punctuation independently, + // e.g. <<Did he say, "let's go home"?>> + // Hence, specifically for English, we treat this special case here. + if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { + for (; j > 0; j--) { + // Here we look to go over any closing punctuation. This is because in dominant + // variants of English, the final period is placed within double quotes and maybe + // other closing punctuation signs. This is generally not true in other languages. + final char c = cs.charAt(j - 1); + if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE + && Character.getType(c) != Character.END_PUNCTUATION) { + break; + } + } + } + + if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes; + char c = cs.charAt(--j); + + // We found the next interesting chunk of text ; next we need to determine if it's the + // end of a sentence. If we have a question mark or an exclamation mark, it's the end of + // a sentence. If it's neither, the only remaining case is the period so we get the opposite + // case out of the way. + if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes; + } + if (c != Constants.CODE_PERIOD || j <= 0) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + } + + // We found out that we have a period. We need to determine if this is a full stop or + // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation + // looks like (\w\.){2,} + // To find out, we will have a simple state machine with the following states : + // START, WORD, PERIOD, ABBREVIATION + // On START : (just before the first period) + // letter => WORD + // whitespace => end with no caps (it was a stand-alone period) + // otherwise => end with caps (several periods/symbols in a row) + // On WORD : (within the word just before the first period) + // letter => WORD + // period => PERIOD + // otherwise => end with caps (it was a word with a full stop at the end) + // On PERIOD : (period within a potential abbreviation) + // letter => LETTER + // otherwise => end with caps (it was not an abbreviation) + // On LETTER : (letter within a potential abbreviation) + // letter => LETTER + // period => PERIOD + // otherwise => end with no caps (it was an abbreviation) + // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This + // should capitalize. + + final int START = 0; + final int WORD = 1; + final int PERIOD = 2; + final int LETTER = 3; + final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS + | TextUtils.CAP_MODE_SENTENCES) & reqModes; + final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + int state = START; + while (j > 0) { + c = cs.charAt(--j); + switch (state) { + case START: + if (Character.isLetter(c)) { + state = WORD; + } else if (Character.isWhitespace(c)) { + return noCaps; + } else { + return caps; + } + break; + case WORD: + if (Character.isLetter(c)) { + state = WORD; + } else if (c == Constants.CODE_PERIOD) { + state = PERIOD; + } else { + return caps; + } + break; + case PERIOD: + if (Character.isLetter(c)) { + state = LETTER; + } else { + return caps; + } + break; + case LETTER: + if (Character.isLetter(c)) { + state = LETTER; + } else if (c == Constants.CODE_PERIOD) { + state = PERIOD; + } else { + return noCaps; + } + } + } + // Here we arrived at the start of the line. This should behave exactly like whitespace. + return (START == state || LETTER == state) ? noCaps : caps; + } +} diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 85cc55232..422448edf 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -215,6 +215,10 @@ public final class Constants { } } + // Constants for CSV parsing. + public static final char CSV_SEPARATOR = ','; + public static final char CSV_ESCAPE = '\\'; + private Constants() { // This utility class is not publicly instantiable. } diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 28ed88a73..97dc6a8ac 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -321,9 +321,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { tempFile.renameTo(file); clearFusionDictionary(); } catch (IOException e) { - Log.e(TAG, "IO exception while writing file: " + e); + Log.e(TAG, "IO exception while writing file", e); } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format: " + e); + Log.e(TAG, "Unsupported format", e); } finally { if (out != null) { try { diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 1c49bb0cc..73ace2bfa 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -1228,7 +1228,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction return; } final String wordToEdit; - if (StringUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) { + if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) { wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); } else { wordToEdit = word; diff --git a/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java b/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java index 9fdbf8703..a8800007a 100644 --- a/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java +++ b/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java @@ -96,7 +96,7 @@ public final class PositionalInfoForUserDictPendingAddition { if (currentCursorPosition != mCursorPos) return true; // We have made all the checks : do the replacement and report success // If this was auto-capitalized, we need to restore the case before committing - final String wordWithCaseFixed = StringUtils.applyAutoCapsMode(mActualWordBeingAdded, + final String wordWithCaseFixed = CapsModeUtils.applyAutoCapsMode(mActualWordBeingAdded, mCapitalizedMode, locale); connection.setComposingRegion(currentCursorPosition - mOriginalWord.length(), currentCursorPosition); diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 521cea977..7300dbd23 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -221,7 +221,7 @@ public final class RichInputConnection { } // This never calls InputConnection#getCapsMode - in fact, it's a static method that // never blocks or initiates IPC. - return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale, + return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale, hasSpaceBefore); } diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java index 728f6b281..d05868029 100644 --- a/java/src/com/android/inputmethod/latin/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/SettingsValues.java @@ -89,8 +89,8 @@ public final class SettingsValues { mWordConnectors = StringUtils.toCodePointArray(res.getString(R.string.symbols_word_connectors)); Arrays.sort(mWordConnectors); - final String[] suggestPuncsSpec = KeySpecParser.parseCsvString( - res.getString(R.string.suggested_punctuations), null); + final String[] suggestPuncsSpec = StringUtils.parseCsvString(res.getString( + R.string.suggested_punctuations)); mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); mWordSeparators = res.getString(R.string.symbols_word_separators); mHintToSaveText = res.getText(R.string.hint_add_to_dictionary); @@ -211,6 +211,7 @@ public final class SettingsValues { final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList(); if (puncs != null) { for (final String puncSpec : puncs) { + // TODO: Stop using KeySpceParser.getLabel(). puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec), SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED, Dictionary.TYPE_HARDCODED)); diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index d00edbe92..90c3fcdd2 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -16,7 +16,6 @@ package com.android.inputmethod.latin; -import android.text.InputType; import android.text.TextUtils; import java.util.ArrayList; @@ -103,37 +102,6 @@ public final class StringUtils { } } - /** - * Apply an auto-caps mode to a string. - * - * This intentionally does NOT apply manual caps mode. It only changes the capitalization if - * the mode is one of the auto-caps modes. - * @param s The string to capitalize. - * @param capitalizeMode The mode in which to capitalize. - * @param locale The locale for capitalizing. - * @return The capitalized string. - */ - public static String applyAutoCapsMode(final String s, final int capitalizeMode, - final Locale locale) { - if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) { - return s.toUpperCase(locale); - } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) { - return toTitleCase(s, locale); - } else { - return s; - } - } - - /** - * Return whether a constant represents an auto-caps mode (either auto-shift or auto-shift-lock) - * @param mode The mode to test for - * @return true if this represents an auto-caps mode, false otherwise - */ - public static boolean isAutoCapsMode(final int mode) { - return WordComposer.CAPS_MODE_AUTO_SHIFTED == mode - || WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == mode; - } - public static String toTitleCase(final String s, final Locale locale) { if (s.length() <= 1) { // TODO: is this really correct? Shouldn't this be s.toUpperCase()? @@ -166,210 +134,41 @@ public final class StringUtils { return codePoints; } - /** - * Determine what caps mode should be in effect at the current offset in - * the text. Only the mode bits set in <var>reqModes</var> will be - * checked. Note that the caps mode flags here are explicitly defined - * to match those in {@link InputType}. - * - * This code is a straight copy of TextUtils.getCapsMode (modulo namespace and formatting - * issues). This will change in the future as we simplify the code for our use and fix bugs. - * - * @param cs The text that should be checked for caps modes. - * @param reqModes The modes to be checked: may be any combination of - * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and - * {@link TextUtils#CAP_MODE_SENTENCES}. - * @param locale The locale to consider for capitalization rules - * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs - * - * @return Returns the actual capitalization modes that can be in effect - * at the current position, which is any combination of - * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and - * {@link TextUtils#CAP_MODE_SENTENCES}. - */ - public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale, - final boolean hasSpaceBefore) { - // Quick description of what we want to do: - // CAP_MODE_CHARACTERS is always on. - // CAP_MODE_WORDS is on if there is some whitespace before the cursor. - // CAP_MODE_SENTENCES is on if there is some whitespace before the cursor, and the end - // of a sentence just before that. - // We ignore opening parentheses and the like just before the cursor for purposes of - // finding whitespace for WORDS and SENTENCES modes. - // The end of a sentence ends with a period, question mark or exclamation mark. If it's - // a period, it also needs not to be an abbreviation, which means it also needs to either - // be immediately preceded by punctuation, or by a string of only letters with single - // periods interleaved. - - // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on. - if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) { - // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already - // evaluated MODE_CHARACTERS, we can return. - return TextUtils.CAP_MODE_CHARACTERS & reqModes; - } - - // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes - // opening parentheses, brackets, opening quotes, everything that *opens* a span of - // text in the linguistic sense. In RTL languages, this is still an opening sign, although - // it may look like a right parenthesis for example. We also include double quote and - // single quote since they aren't start punctuation in the unicode sense, but should still - // be skipped for English. TODO: does this depend on the language? - int i; - if (hasSpaceBefore) { - i = cs.length() + 1; - } else { - for (i = cs.length(); i > 0; i--) { - final char c = cs.charAt(i - 1); - if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE - && Character.getType(c) != Character.START_PUNCTUATION) { - break; + public static String[] parseCsvString(final String text) { + final int size = text.length(); + if (size == 0) { + return null; + } + if (codePointCount(text) == 1) { + return text.codePointAt(0) == Constants.CSV_SEPARATOR ? null : new String[] { text }; + } + + ArrayList<String> list = null; + int start = 0; + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (c == Constants.CSV_SEPARATOR) { + // Skip empty entry. + if (pos - start > 0) { + if (list == null) { + list = CollectionUtils.newArrayList(); + } + list.add(text.substring(start, pos)); } + // Skip comma + start = pos + 1; + } else if (c == Constants.CSV_ESCAPE) { + // Skip escape character and escaped character. + pos++; } } - - // We are now on the character that precedes any starting punctuation, so in the most - // frequent case this will be whitespace or a letter, although it may occasionally be a - // start of line, or some symbol. - - // Step 3 : Search for the start of a paragraph. From the starting point computed in step 2, - // we go back over any space or tab char sitting there. We find the start of a paragraph - // if the first char that's not a space or tab is a start of line (as in \n, start of text, - // or some other similar characters). - int j = i; - char prevChar = Constants.CODE_SPACE; - if (hasSpaceBefore) --j; - while (j > 0) { - prevChar = cs.charAt(j - 1); - if (!Character.isSpaceChar(prevChar) && prevChar != Constants.CODE_TAB) break; - j--; - } - if (j <= 0 || Character.isWhitespace(prevChar)) { - // There are only spacing chars between the start of the paragraph and the cursor, - // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both - // MODE_WORDS and MODE_SENTENCES should be active. - return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS - | TextUtils.CAP_MODE_SENTENCES) & reqModes; - } - if (i == j) { - // If we don't have whitespace before index i, it means neither MODE_WORDS - // nor mode sentences should be on so we can return right away. - return TextUtils.CAP_MODE_CHARACTERS & reqModes; - } - if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) { - // Here we know we have whitespace before the cursor (if not, we returned in the above - // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate - // MODE_SENTENCES so we can return right away. - return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; - } - // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above, - // we know that MODE_SENTENCES is being requested. - - // Step 4 : Search for MODE_SENTENCES. - // English is a special case in that "American typography" rules, which are the most common - // in English, state that a sentence terminator immediately following a quotation mark - // should be swapped with it and de-duplicated (included in the quotation mark), - // e.g. <<Did he say, "let's go home?">> - // No other language has such a rule as far as I know, instead putting inside the quotation - // mark as the exact thing quoted and handling the surrounding punctuation independently, - // e.g. <<Did he say, "let's go home"?>> - // Hence, specifically for English, we treat this special case here. - if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { - for (; j > 0; j--) { - // Here we look to go over any closing punctuation. This is because in dominant - // variants of English, the final period is placed within double quotes and maybe - // other closing punctuation signs. This is generally not true in other languages. - final char c = cs.charAt(j - 1); - if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE - && Character.getType(c) != Character.END_PUNCTUATION) { - break; - } - } - } - - if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes; - char c = cs.charAt(--j); - - // We found the next interesting chunk of text ; next we need to determine if it's the - // end of a sentence. If we have a question mark or an exclamation mark, it's the end of - // a sentence. If it's neither, the only remaining case is the period so we get the opposite - // case out of the way. - if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) { - return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes; - } - if (c != Constants.CODE_PERIOD || j <= 0) { - return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + final String remain = (size - start > 0) ? text.substring(start) : null; + if (list == null) { + return remain != null ? new String[] { remain } : null; } - - // We found out that we have a period. We need to determine if this is a full stop or - // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation - // looks like (\w\.){2,} - // To find out, we will have a simple state machine with the following states : - // START, WORD, PERIOD, ABBREVIATION - // On START : (just before the first period) - // letter => WORD - // whitespace => end with no caps (it was a stand-alone period) - // otherwise => end with caps (several periods/symbols in a row) - // On WORD : (within the word just before the first period) - // letter => WORD - // period => PERIOD - // otherwise => end with caps (it was a word with a full stop at the end) - // On PERIOD : (period within a potential abbreviation) - // letter => LETTER - // otherwise => end with caps (it was not an abbreviation) - // On LETTER : (letter within a potential abbreviation) - // letter => LETTER - // period => PERIOD - // otherwise => end with no caps (it was an abbreviation) - // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This - // should capitalize. - - final int START = 0; - final int WORD = 1; - final int PERIOD = 2; - final int LETTER = 3; - final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS - | TextUtils.CAP_MODE_SENTENCES) & reqModes; - final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; - int state = START; - while (j > 0) { - c = cs.charAt(--j); - switch (state) { - case START: - if (Character.isLetter(c)) { - state = WORD; - } else if (Character.isWhitespace(c)) { - return noCaps; - } else { - return caps; - } - break; - case WORD: - if (Character.isLetter(c)) { - state = WORD; - } else if (c == Constants.CODE_PERIOD) { - state = PERIOD; - } else { - return caps; - } - break; - case PERIOD: - if (Character.isLetter(c)) { - state = LETTER; - } else { - return caps; - } - break; - case LETTER: - if (Character.isLetter(c)) { - state = LETTER; - } else if (c == Constants.CODE_PERIOD) { - state = PERIOD; - } else { - return noCaps; - } - } + if (remain != null) { + list.add(remain); } - // Here we arrived at the start of the line. This should behave exactly like whitespace. - return (START == state || LETTER == state) ? noCaps : caps; + return list.toArray(new String[list.size()]); } } diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java index eb5c387a8..62f2a9750 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java @@ -122,9 +122,9 @@ public final class UserHistoryDictIOUtils { BinaryDictInputOutput.writeDictionaryBinary(destination, fusionDict, formatOptions); Log.d(TAG, "end writing"); } catch (IOException e) { - Log.e(TAG, "IO exception while writing file: " + e); + Log.e(TAG, "IO exception while writing file", e); } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported fomat: " + e); + Log.e(TAG, "Unsupported format", e); } } @@ -184,11 +184,11 @@ public final class UserHistoryDictIOUtils { BinaryDictIOUtils.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies, bigrams); } catch (IOException e) { - Log.e(TAG, "IO exception while reading file: " + e); + Log.e(TAG, "IO exception while reading file", e); } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format: " + e); + Log.e(TAG, "Unsupported format", e); } catch (ArrayIndexOutOfBoundsException e) { - Log.e(TAG, "ArrayIndexOutOfBoundsException while reading file: " + e); + Log.e(TAG, "ArrayIndexOutOfBoundsException while reading file", e); } addWordsFromWordMap(unigrams, frequencies, bigrams, dict); } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index b0e471643..cd3f9e442 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -352,7 +352,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { if (DBG) { throw e; } else { - Log.e(TAG, "Exception while spellcheking: " + e); + Log.e(TAG, "Exception while spellcheking", e); return AndroidSpellCheckerService.getNotInDictEmptySuggestions(); } } diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 3d825e1ee..45212913e 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -763,18 +763,26 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (isIncludingRecording) { // Try to read recording from recently written json file if (mUserRecordingFile != null) { + FileChannel channel = null; try { - final FileChannel channel = - new FileInputStream(mUserRecordingFile).getChannel(); + channel = new FileInputStream(mUserRecordingFile).getChannel(); final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); // Android's openFileOutput() creates the file, so we use Android's default // Charset (UTF-8) here to read it. recording = Charset.defaultCharset().decode(buffer).toString(); } catch (FileNotFoundException e) { - e.printStackTrace(); + Log.e(TAG, "Could not find recording file", e); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Error reading recording file", e); + } finally { + if (channel != null) { + try { + channel.close(); + } catch (IOException e) { + Log.e(TAG, "Error closing recording file", e); + } + } } } } |