diff options
Diffstat (limited to 'java/src')
21 files changed, 473 insertions, 340 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 d4cdc6c5c..165116ae0 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -16,14 +16,19 @@ package com.android.inputmethod.latin; +import android.content.ContentProviderClient; import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; +import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.latin.DictionaryInfoUtils.DictionaryInfo; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; @@ -32,6 +37,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -68,6 +74,10 @@ public final class BinaryDictionaryFileDumper { // The path fragment to append after the client ID for dictionary info requests. private static final String QUERY_PATH_DICT_INFO = "dict"; + // The path fragment to append after the client ID for updating the metadata URI. + private static final String QUERY_PATH_METADATA = "metadata"; + private static final String INSERT_METADATA_CLIENT_ID_COLUMN = "clientid"; + private static final String INSERT_METADATA_METADATA_URI_COLUMN = "uri"; // Prevents this class to be accidentally instantiated. private BinaryDictionaryFileDumper() { @@ -91,25 +101,31 @@ public final class BinaryDictionaryFileDumper { */ 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) return Collections.<WordListInfo>emptyList(); - if (c.getCount() <= 0 || !c.moveToFirst()) { - c.close(); - return Collections.<WordListInfo>emptyList(); - } - + final ContentProviderClient client = context.getContentResolver(). + acquireContentProviderClient(getProviderUriBuilder("").build()); + if (null == client) return Collections.<WordListInfo>emptyList(); try { + final Cursor c = client.query(dictionaryPackUri, DICTIONARY_PROJECTION, null, null, + null); + if (null == c) { + reinitializeClientRecordInDictionaryContentProvider(context, client, 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); @@ -119,11 +135,20 @@ public final class BinaryDictionaryFileDumper { } while (c.moveToNext()); c.close(); return list; + } catch (RemoteException e) { + // The documentation is unclear as to in which cases this may happen, but it probably + // happens when the content provider got suddenly killed because it crashed or because + // the user disabled it through Settings. + Log.e(TAG, "RemoteException: communication with the dictionary pack cut", 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); + // 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 we don't want + // to die no matter what. So let's be as safe as possible. + Log.e(TAG, "Unexpected exception communicating with the dictionary pack", e); return Collections.<WordListInfo>emptyList(); + } finally { + client.release(); } } @@ -237,7 +262,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 @@ -255,12 +280,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); } } } @@ -335,4 +360,35 @@ public final class BinaryDictionaryFileDumper { output.write(buffer, 0, readBytes); input.close(); } + + private static void reinitializeClientRecordInDictionaryContentProvider(final Context context, + final ContentProviderClient client, final String clientId) throws RemoteException { + final String metadataFileUri = context.getString(R.string.dictionary_pack_metadata_uri); + if (TextUtils.isEmpty(metadataFileUri)) return; + // Tell the content provider to reset all information about this client id + final Uri metadataContentUri = getProviderUriBuilder(clientId) + .appendPath(QUERY_PATH_METADATA) + .appendQueryParameter(QUERY_PARAMETER_PROTOCOL, QUERY_PARAMETER_PROTOCOL_VALUE) + .build(); + client.delete(metadataContentUri, null, null); + // Update the metadata URI + final ContentValues metadataValues = new ContentValues(); + metadataValues.put(INSERT_METADATA_CLIENT_ID_COLUMN, clientId); + metadataValues.put(INSERT_METADATA_METADATA_URI_COLUMN, metadataFileUri); + client.insert(metadataContentUri, metadataValues); + + // Update the dictionary list. + final Uri dictionaryContentUriBase = getProviderUriBuilder(clientId) + .appendPath(QUERY_PATH_DICT_INFO) + .appendQueryParameter(QUERY_PARAMETER_PROTOCOL, QUERY_PARAMETER_PROTOCOL_VALUE) + .build(); + final ArrayList<DictionaryInfo> dictionaryList = + DictionaryInfoUtils.getCurrentDictionaryFileNameAndVersionInfo(context); + final int length = dictionaryList.size(); + for (int i = 0; i < length; ++i) { + final DictionaryInfo info = dictionaryList.get(i); + client.insert(Uri.withAppendedPath(dictionaryContentUriBase, info.mId), + info.toContentValues()); + } + } } 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/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java index 8f16a8e4d..d2a946bf5 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java @@ -16,9 +16,11 @@ package com.android.inputmethod.latin; +import android.content.ContentValues; import android.content.Context; import android.content.res.AssetManager; import android.content.res.Resources; +import android.text.format.DateUtils; import android.util.Log; import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; @@ -39,19 +41,40 @@ public class DictionaryInfoUtils { private static final String RESOURCE_PACKAGE_NAME = DictionaryInfoUtils.class.getPackage().getName(); private static final String DEFAULT_MAIN_DICT = "main"; + private static final String ID_CATEGORY_SEPARATOR = + BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR; private static final String MAIN_DICT_PREFIX = "main_"; // 6 digits - unicode is limited to 21 bits private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6; public static class DictionaryInfo { + private static final String LOCALE_COLUMN = "locale"; + private static final String WORDLISTID_COLUMN = "id"; + private static final String LOCAL_FILENAME_COLUMN = "filename"; + private static final String DATE_COLUMN = "date"; + private static final String FILESIZE_COLUMN = "filesize"; + private static final String VERSION_COLUMN = "version"; public final Locale mLocale; public final AssetFileAddress mFileAddress; public final int mVersion; + public final String mId; public DictionaryInfo(final Locale locale, final AssetFileAddress fileAddress, final int version) { mLocale = locale; mFileAddress = fileAddress; mVersion = version; + mId = DEFAULT_MAIN_DICT + ID_CATEGORY_SEPARATOR + mLocale; + } + public ContentValues toContentValues() { + final ContentValues values = new ContentValues(); + values.put(WORDLISTID_COLUMN, mId); + values.put(LOCALE_COLUMN, mLocale.toString()); + values.put(LOCAL_FILENAME_COLUMN, mFileAddress.mFilename); + values.put(DATE_COLUMN, + new File(mFileAddress.mFilename).lastModified() / DateUtils.SECOND_IN_MILLIS); + values.put(FILESIZE_COLUMN, mFileAddress.mLength); + values.put(VERSION_COLUMN, mVersion); + return values; } } @@ -284,21 +307,23 @@ public class DictionaryInfoUtils { // Retrieve downloaded dictionaries final File[] directoryList = getCachedDirectoryList(context); - for (final File directory : directoryList) { - final String localeString = getWordListIdFromFileName(directory.getName()); - File[] dicts = BinaryDictionaryGetter.getCachedWordLists(localeString, context); - for (final File dict : dicts) { - final String wordListId = getWordListIdFromFileName(dict.getName()); - if (!DictionaryInfoUtils.isMainWordListId(wordListId)) continue; - final Locale locale = LocaleUtils.constructLocaleFromString(localeString); - final AssetFileAddress fileAddress = AssetFileAddress.makeFromFile(dict); - final DictionaryInfo dictionaryInfo = - createDictionaryInfoFromFileAddress(fileAddress); - // Protect against cases of a less-specific dictionary being found, like an - // en dictionary being used for an en_US locale. In this case, the en dictionary - // should be used for en_US but discounted for listing purposes. - if (!dictionaryInfo.mLocale.equals(locale)) continue; - addOrUpdateDictInfo(dictList, dictionaryInfo); + if (null != directoryList) { + for (final File directory : directoryList) { + final String localeString = getWordListIdFromFileName(directory.getName()); + File[] dicts = BinaryDictionaryGetter.getCachedWordLists(localeString, context); + for (final File dict : dicts) { + final String wordListId = getWordListIdFromFileName(dict.getName()); + if (!DictionaryInfoUtils.isMainWordListId(wordListId)) continue; + final Locale locale = LocaleUtils.constructLocaleFromString(localeString); + final AssetFileAddress fileAddress = AssetFileAddress.makeFromFile(dict); + final DictionaryInfo dictionaryInfo = + createDictionaryInfoFromFileAddress(fileAddress); + // Protect against cases of a less-specific dictionary being found, like an + // en dictionary being used for an en_US locale. In this case, the en dictionary + // should be used for en_US but discounted for listing purposes. + if (!dictionaryInfo.mLocale.equals(locale)) continue; + addOrUpdateDictInfo(dictList, dictionaryInfo); + } } } 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/LogStatement.java b/java/src/com/android/inputmethod/research/LogStatement.java index 059146ae6..09b12fcfa 100644 --- a/java/src/com/android/inputmethod/research/LogStatement.java +++ b/java/src/com/android/inputmethod/research/LogStatement.java @@ -35,7 +35,7 @@ import java.io.IOException; * associated with the {@code String[] keys} are likely to reveal information about the user. The * actual values are stored separately. */ -class LogStatement { +public class LogStatement { private static final String TAG = LogStatement.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; @@ -166,6 +166,8 @@ class LogStatement { /** * Write the contents out through jsonWriter. * + * The JsonWriter class must have already had {@code JsonWriter.beginArray} called on it. + * * Note that this method is not thread safe for the same jsonWriter. Callers must ensure * thread safety. */ diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java index a584a3af6..1a9a720f3 100644 --- a/java/src/com/android/inputmethod/research/LogUnit.java +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -110,7 +110,13 @@ import java.util.List; } /** - * Publish the contents of this LogUnit to researchLog. + * Publish the contents of this LogUnit to {@code researchLog}. + * + * For each publishable {@code LogStatement}, invoke {@link LogStatement#outputToLocked}. + * + * @param researchLog where to publish the contents of this {@code LogUnit} + * @param canIncludePrivateData whether the private data in this {@code LogUnit} should be + * included */ public synchronized void publishTo(final ResearchLog researchLog, final boolean canIncludePrivateData) { diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index 24bf7d15f..5114977d8 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -81,10 +81,7 @@ public class ResearchLog { } } - public ResearchLog(final File outputFile, Context context) { - if (outputFile == null) { - throw new IllegalArgumentException(); - } + public ResearchLog(final File outputFile, final Context context) { mExecutor = Executors.newSingleThreadScheduledExecutor(); mFile = outputFile; mContext = context; @@ -112,7 +109,7 @@ public class ResearchLog { Log.d(TAG, "error when closing ResearchLog:"); e.printStackTrace(); } finally { - if (mFile.exists()) { + if (mFile != null && mFile.exists()) { mFile.setWritable(false, false); } if (onClosed != null) { @@ -139,7 +136,9 @@ public class ResearchLog { mHasWrittenData = false; } } finally { - mIsAbortSuccessful = mFile.delete(); + if (mFile != null) { + mIsAbortSuccessful = mFile.delete(); + } } return null; } @@ -209,7 +208,7 @@ public class ResearchLog { */ public JsonWriter getValidJsonWriterLocked() { try { - if (mJsonWriter == NULL_JSON_WRITER) { + if (mJsonWriter == NULL_JSON_WRITER && mFile != null) { final FileOutputStream fos = mContext.openFileOutput(mFile.getName(), Context.MODE_PRIVATE); mJsonWriter = new JsonWriter(new BufferedWriter(new OutputStreamWriter(fos))); diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 364ab2da2..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); + } + } } } } @@ -1395,7 +1403,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang */ private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY = new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index", - "suggestion", "x", "y"); + "suggestion", "x", "y", "isBatchMode"); public static void latinIME_pickSuggestionManually(final String replacedWord, final int index, final String suggestion, final boolean isBatchMode) { final ResearchLogger researchLogger = getInstance(); @@ -1408,7 +1416,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY, scrubDigitsFromString(replacedWord), index, suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE, - Constants.SUGGESTION_STRIP_COORDINATE); + Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode); researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis()); } |