diff options
Diffstat (limited to 'java/src/com/android/inputmethod')
54 files changed, 1616 insertions, 744 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java index b84d402fb..94a1ee6eb 100644 --- a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java +++ b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java @@ -121,7 +121,7 @@ public final class MainKeyboardAccessibilityDelegate */ private void announceKeyboardLanguage(final Keyboard keyboard) { final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale( - keyboard.mId.mSubtype); + keyboard.mId.mSubtype.getRawSubtype()); sendWindowStateChanged(languageText); } diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java index 365867257..b9a536721 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java @@ -21,6 +21,7 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.RichInputMethodSubtype; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -64,6 +65,10 @@ public final class InputMethodSubtypeCompatUtils { overridesImplicitlyEnabledSubtype, id); } + public static boolean isAsciiCapable(final RichInputMethodSubtype subtype) { + return isAsciiCapable(subtype.getRawSubtype()); + } + public static boolean isAsciiCapable(final InputMethodSubtype subtype) { return isAsciiCapableWithAPI(subtype) || subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE); diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java index 3d294acd7..3aa026e77 100644 --- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java +++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java @@ -120,9 +120,10 @@ public final class ActionBatch { if (MetadataDbHelper.STATUS_DOWNLOADING == status) { // The word list is still downloading. Cancel the download and revert the // word list status to "available". - manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); + manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); - } else if (MetadataDbHelper.STATUS_AVAILABLE != status) { + } else if (MetadataDbHelper.STATUS_AVAILABLE != status + && MetadataDbHelper.STATUS_RETRYING != status) { // Should never happen Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " + status + " for an upgrade action. Fall back to download."); @@ -325,8 +326,8 @@ public final class ActionBatch { mWordList.mId, mWordList.mLocale, mWordList.mDescription, null == mWordList.mLocalFilename ? "" : mWordList.mLocalFilename, mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, + mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Insert 'available' record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); @@ -374,9 +375,9 @@ public final class ActionBatch { final ContentValues values = MetadataDbHelper.makeContentValues(0, MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED, mWordList.mId, mWordList.mLocale, mWordList.mDescription, - "", mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + "", mWordList.mRemoteFilename, mWordList.mLastUpdate, + mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mRetryCount, + mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); @@ -417,8 +418,8 @@ public final class ActionBatch { mWordList.mId, mWordList.mLocale, mWordList.mDescription, oldValues.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN), mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, + mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Updating record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.update(MetadataDbHelper.METADATA_TABLE_NAME, values, diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java index f5bd84c8c..e748321e2 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java @@ -470,7 +470,11 @@ public final class DictionaryProvider extends ContentProvider { } else if (MetadataDbHelper.STATUS_INSTALLED == status) { final String result = uri.getQueryParameter(QUERY_PARAMETER_DELETE_RESULT); if (QUERY_PARAMETER_FAILURE.equals(result)) { - UpdateHandler.markAsBroken(getContext(), clientId, wordlistId, version); + if (DEBUG) { + Log.d(TAG, + "Dictionary is broken, attempting to retry download & installation."); + } + UpdateHandler.markAsBrokenOrRetrying(getContext(), clientId, wordlistId, version); } final String localFilename = wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java index 17dd781d5..e9dde4245 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java @@ -47,10 +47,13 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // used to identify the versions for upgrades. This should never change going forward. private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6; // The current database version. - private static final int CURRENT_METADATA_DATABASE_VERSION = 9; + private static final int CURRENT_METADATA_DATABASE_VERSION = 10; private final static long NOT_A_DOWNLOAD_ID = -1; + // The number of retries allowed when attempting to download a broken dictionary. + public static final int DICTIONARY_RETRY_THRESHOLD = 2; + public static final String METADATA_TABLE_NAME = "pendingUpdates"; static final String CLIENT_TABLE_NAME = "clients"; public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID @@ -68,7 +71,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static final String FORMATVERSION_COLUMN = "formatversion"; public static final String FLAGS_COLUMN = "flags"; public static final String RAW_CHECKSUM_COLUMN = "rawChecksum"; - public static final int COLUMN_COUNT = 14; + public static final String RETRY_COUNT_COLUMN = "remainingRetries"; + public static final int COLUMN_COUNT = 15; private static final String CLIENT_CLIENT_ID_COLUMN = "clientid"; private static final String CLIENT_METADATA_URI_COLUMN = "uri"; @@ -98,6 +102,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // Deleting: the user marked this word list to be deleted, but it has not been yet because // Latin IME is not up yet. public static final int STATUS_DELETING = 5; + // Retry: dictionary got corrupted, so an attempt must be done to download & install it again. + public static final int STATUS_RETRYING = 6; // Types, for storing in the TYPE_COLUMN // This is metadata about what is available. @@ -124,6 +130,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { + FORMATVERSION_COLUMN + " INTEGER, " + FLAGS_COLUMN + " INTEGER, " + RAW_CHECKSUM_COLUMN + " TEXT," + + RETRY_COUNT_COLUMN + " INTEGER, " + "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));"; private static final String METADATA_CREATE_CLIENT_TABLE = "CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " (" @@ -140,7 +147,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN, LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN, - RAW_CHECKSUM_COLUMN }; + RAW_CHECKSUM_COLUMN, RETRY_COUNT_COLUMN }; // List of all client table columns. static final String[] CLIENT_TABLE_COLUMNS = { CLIENT_CLIENT_ID_COLUMN, CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN }; @@ -219,7 +226,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { createClientTable(db); } - private void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db, final String clientId) { + private void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db) { try { db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM " + METADATA_TABLE_NAME + " LIMIT 0;"); @@ -230,6 +237,17 @@ public class MetadataDbHelper extends SQLiteOpenHelper { } } + private void addRetryCountColumnUnlessPresent(final SQLiteDatabase db) { + try { + db.execSQL("SELECT " + RETRY_COUNT_COLUMN + " FROM " + + METADATA_TABLE_NAME + " LIMIT 0;"); + } catch (SQLiteException e) { + Log.i(TAG, "No " + RETRY_COUNT_COLUMN + " column : creating it"); + db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN " + + RETRY_COUNT_COLUMN + " INTEGER DEFAULT " + DICTIONARY_RETRY_THRESHOLD + ";"); + } + } + /** * Upgrade the database. Upgrade from version 3 is supported. * Version 3 has a DB named METADATA_DATABASE_NAME_STEM containing a table METADATA_TABLE_NAME. @@ -280,7 +298,14 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // strengthen the system against corrupted dictionary files. // The most secure way to upgrade a database is to just test for the column presence, and // add it if it's not there. - addRawChecksumColumnUnlessPresent(db, mClientId); + addRawChecksumColumnUnlessPresent(db); + + // A retry count column that did not exist in the previous versions was added that + // corresponds to the number of download & installation attempts that have been made + // in order to strengthen the system recovery from corrupted dictionary files. + // The most secure way to upgrade a database is to just test for the column presence, and + // add it if it's not there. + addRetryCountColumnUnlessPresent(db); } /** @@ -452,8 +477,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static ContentValues makeContentValues(final int pendingId, final int type, final int status, final String wordlistId, final String locale, final String description, final String filename, final String url, final long date, - final String rawChecksum, final String checksum, final long filesize, final int version, - final int formatVersion) { + final String rawChecksum, final String checksum, final int retryCount, + final long filesize, final int version, final int formatVersion) { final ContentValues result = new ContentValues(COLUMN_COUNT); result.put(PENDINGID_COLUMN, pendingId); result.put(TYPE_COLUMN, type); @@ -465,6 +490,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { result.put(REMOTE_FILENAME_COLUMN, url); result.put(DATE_COLUMN, date); result.put(RAW_CHECKSUM_COLUMN, rawChecksum); + result.put(RETRY_COUNT_COLUMN, retryCount); result.put(CHECKSUM_COLUMN, checksum); result.put(FILESIZE_COLUMN, filesize); result.put(VERSION_COLUMN, version); @@ -502,6 +528,9 @@ public class MetadataDbHelper extends SQLiteOpenHelper { if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0); // Raw checksum unknown unless specified if (null == result.get(RAW_CHECKSUM_COLUMN)) result.put(RAW_CHECKSUM_COLUMN, ""); + // Retry column 0 unless specified + if (null == result.get(RETRY_COUNT_COLUMN)) result.put(RETRY_COUNT_COLUMN, + DICTIONARY_RETRY_THRESHOLD); // Checksum unknown unless specified if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, ""); // No filesize unless specified @@ -551,6 +580,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { putIntResult(result, cursor, DATE_COLUMN); putStringResult(result, cursor, RAW_CHECKSUM_COLUMN); putStringResult(result, cursor, CHECKSUM_COLUMN); + putIntResult(result, cursor, RETRY_COUNT_COLUMN); putIntResult(result, cursor, FILESIZE_COLUMN); putIntResult(result, cursor, VERSION_COLUMN); putIntResult(result, cursor, FORMATVERSION_COLUMN); @@ -676,8 +706,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper { final String id, final int version) { final Cursor cursor = db.query(METADATA_TABLE_NAME, METADATA_TABLE_COLUMNS, - WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ?", - new String[] { id, Integer.toString(version) }, null, null, null); + WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ? AND " + + FORMATVERSION_COLUMN + "<= ?", + new String[] + { id, + Integer.toString(version), + Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION) + }, + null /* groupBy */, + null /* having */, + FORMATVERSION_COLUMN + " DESC"/* orderBy */); if (null == cursor) { return null; } @@ -706,7 +744,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { return null; } try { - // This is a lookup by primary key, so there can't be more than one result. + // Return the first result from the list of results. return getFirstLineAsContentValues(cursor); } finally { cursor.close(); @@ -1085,4 +1123,27 @@ public class MetadataDbHelper extends SQLiteOpenHelper { final int version) { markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID); } + + /** + * Checks retry counts and marks the word list as retrying if retry is possible. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + * @return {@code true} if the retry is possible. + */ + public static boolean maybeMarkEntryAsRetrying(final SQLiteDatabase db, final String id, + final int version) { + final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version); + int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN); + if (retryCount > 1) { + values.put(STATUS_COLUMN, STATUS_RETRYING); + values.put(RETRY_COUNT_COLUMN, retryCount - 1); + db.update(METADATA_TABLE_NAME, values, + WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?", + new String[] { id, Integer.toString(version) }); + return true; + } + return false; + } } diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java index d66b69050..639d904a0 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java @@ -16,6 +16,7 @@ package com.android.inputmethod.dictionarypack; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -55,6 +56,7 @@ public class MetadataHandler { final int rawChecksumIndex = results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN); final int checksumIndex = results.getColumnIndex(MetadataDbHelper.CHECKSUM_COLUMN); + final int retryCountIndex = results.getColumnIndex(MetadataDbHelper.RETRY_COUNT_COLUMN); final int localFilenameIndex = results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN); final int remoteFilenameIndex = @@ -70,6 +72,7 @@ public class MetadataHandler { results.getLong(fileSizeIndex), results.getString(rawChecksumIndex), results.getString(checksumIndex), + results.getInt(retryCountIndex), results.getString(localFilenameIndex), results.getString(remoteFilenameIndex), results.getInt(versionIndex), @@ -102,6 +105,22 @@ public class MetadataHandler { } /** + * Gets the metadata, for a specific dictionary. + * + * @param context The context to open files over. + * @param clientId the client id for retrieving the database. null for default (deprecated). + * @param wordListId the word list ID. + * @param version the word list version. + * @return the current metaData + */ + public static WordListMetadata getCurrentMetadataForWordList(final Context context, + final String clientId, final String wordListId, final int version) { + final ContentValues contentValues = MetadataDbHelper.getContentValuesByWordListId( + MetadataDbHelper.getDb(context, clientId), wordListId, version); + return WordListMetadata.createFromContentValues(contentValues); + } + + /** * Read metadata from a stream. * @param input The stream to read from. * @return The read metadata. diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java b/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java index 52290cadc..2b67ae9ff 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java @@ -83,6 +83,7 @@ public class MetadataParser { Long.parseLong(arguments.get(FILESIZE_FIELD_NAME)), arguments.get(RAW_CHECKSUM_FIELD_NAME), arguments.get(CHECKSUM_FIELD_NAME), + MetadataDbHelper.DICTIONARY_RETRY_THRESHOLD /* retryCount */, null, arguments.get(REMOTE_FILENAME_FIELD_NAME), Integer.parseInt(arguments.get(VERSION_FIELD_NAME)), diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index 6fbca44c5..d8c5f3165 100644 --- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java @@ -31,7 +31,6 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.ConnectivityManager; import android.net.Uri; -import android.os.Build; import android.os.ParcelFileDescriptor; import android.text.TextUtils; import android.util.Log; @@ -785,6 +784,10 @@ public final class UpdateHandler { } else { final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); if (newInfo.mVersion == currentInfo.mVersion) { + if (newInfo.mRemoteFilename == currentInfo.mRemoteFilename) { + // If the dictionary url hasn't changed, we should preserve the retryCount. + newInfo.mRetryCount = currentInfo.mRetryCount; + } // If it's the same id/version, we update the DB with the new values. // It doesn't matter too much if they didn't change. actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo)); @@ -987,16 +990,17 @@ public final class UpdateHandler { public static void markAsUsed(final Context context, final String clientId, final String wordlistId, final int version, final int status, final boolean allowDownloadOnMeteredData) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - WordListMetadata wordList = MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + final ActionBatch actions = new ActionBatch(); if (MetadataDbHelper.STATUS_DISABLED == status || MetadataDbHelper.STATUS_DELETING == status) { - actions.add(new ActionBatch.EnableAction(clientId, wordList)); + actions.add(new ActionBatch.EnableAction(clientId, wordListMetaData)); } else if (MetadataDbHelper.STATUS_AVAILABLE == status) { - actions.add(new ActionBatch.StartDownloadAction(clientId, wordList, + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData, allowDownloadOnMeteredData)); } else { Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status); @@ -1022,13 +1026,13 @@ public final class UpdateHandler { // markAsUsed for consistency. public static void markAsUnused(final Context context, final String clientId, final String wordlistId, final int version, final int status) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - final WordListMetadata wordList = - MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.DisableAction(clientId, wordList)); + actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); actions.execute(context, new LogProblemReporter(TAG)); signalNewDictionaryState(context); } @@ -1051,14 +1055,14 @@ public final class UpdateHandler { */ public static void markAsDeleting(final Context context, final String clientId, final String wordlistId, final int version, final int status) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - final WordListMetadata wordList = - MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.DisableAction(clientId, wordList)); - actions.add(new ActionBatch.StartDeleteAction(clientId, wordList)); + actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); + actions.add(new ActionBatch.StartDeleteAction(clientId, wordListMetaData)); actions.execute(context, new LogProblemReporter(TAG)); signalNewDictionaryState(context); } @@ -1076,33 +1080,47 @@ public final class UpdateHandler { */ public static void markAsDeleted(final Context context, final String clientId, final String wordlistId, final int version, final int status) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - final WordListMetadata wordList = - MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.FinishDeleteAction(clientId, wordList)); + actions.add(new ActionBatch.FinishDeleteAction(clientId, wordListMetaData)); actions.execute(context, new LogProblemReporter(TAG)); signalNewDictionaryState(context); } /** - * Marks the word list with the passed id as broken. - * - * This effectively deletes the entry from the metadata. It doesn't prevent the same - * word list to be downloaded again at a later time if the same or a new version is - * available the next time we download the metadata. + * Checks whether the word list should be downloaded again; in which case an download & + * installation attempt is made. Otherwise the word list is marked broken. * * @param context the context to open the database on. * @param clientId the id of the client. - * @param wordlistId the id of the word list to mark as broken. - * @param version the version of the word list to mark as deleted. + * @param wordlistId the id of the word list which is broken. + * @param version the version of the broken word list. */ - public static void markAsBroken(final Context context, final String clientId, + public static void markAsBrokenOrRetrying(final Context context, final String clientId, final String wordlistId, final int version) { - // TODO: do this on another thread to avoid blocking the UI. - MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), - wordlistId, version); + boolean isRetryPossible = MetadataDbHelper.maybeMarkEntryAsRetrying( + MetadataDbHelper.getDb(context, clientId), wordlistId, version); + + if (isRetryPossible) { + if (DEBUG) { + Log.d(TAG, "Attempting to download & install the wordlist again."); + } + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + final ActionBatch actions = new ActionBatch(); + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData, false)); + actions.execute(context, new LogProblemReporter(TAG)); + } else { + if (DEBUG) { + Log.d(TAG, "Retries for wordlist exhausted, deleting the wordlist from table."); + } + MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), + wordlistId, version); + } } } diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java index 9e510a68b..59f75e4ed 100644 --- a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java +++ b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java @@ -36,6 +36,7 @@ public class WordListMetadata { public final String mRemoteFilename; public final int mVersion; // version of this word list public final int mFlags; // Always 0 in this version, reserved for future use + public int mRetryCount; // The locale is matched against the locale requested by the client. The matching algorithm // is a standard locale matching with fallback; it is implemented in @@ -51,8 +52,9 @@ public class WordListMetadata { public WordListMetadata(final String id, final int type, final String description, final long lastUpdate, final long fileSize, - final String rawChecksum, final String checksum, final String localFilename, - final String remoteFilename, final int version, final int formatVersion, + final String rawChecksum, final String checksum, final int retryCount, + final String localFilename, final String remoteFilename, + final int version, final int formatVersion, final int flags, final String locale) { mId = id; mType = type; @@ -61,6 +63,7 @@ public class WordListMetadata { mFileSize = fileSize; mRawChecksum = rawChecksum; mChecksum = checksum; + mRetryCount = retryCount; mLocalFilename = localFilename; mRemoteFilename = remoteFilename; mVersion = version; @@ -82,6 +85,7 @@ public class WordListMetadata { final Long fileSize = values.getAsLong(MetadataDbHelper.FILESIZE_COLUMN); final String rawChecksum = values.getAsString(MetadataDbHelper.RAW_CHECKSUM_COLUMN); final String checksum = values.getAsString(MetadataDbHelper.CHECKSUM_COLUMN); + final int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN); final String localFilename = values.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); final String remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN); final Integer version = values.getAsInteger(MetadataDbHelper.VERSION_COLUMN); @@ -103,7 +107,8 @@ public class WordListMetadata { throw new IllegalArgumentException(); } return new WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum, - checksum, localFilename, remoteFilename, version, formatVersion, flags, locale); + checksum, retryCount, localFilename, remoteFilename, version, formatVersion, + flags, locale); } @Override @@ -116,6 +121,7 @@ public class WordListMetadata { sb.append("\nFileSize : ").append(mFileSize); sb.append("\nRawChecksum : ").append(mRawChecksum); sb.append("\nChecksum : ").append(mChecksum); + sb.append("\nRetryCount: ").append(mRetryCount); sb.append("\nLocalFilename : ").append(mLocalFilename); sb.append("\nRemoteFilename : ").append(mRemoteFilename); sb.append("\nVersion : ").append(mVersion); diff --git a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java index 4f3f4d25f..88c70630d 100644 --- a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java +++ b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java @@ -17,10 +17,12 @@ package com.android.inputmethod.event; import android.text.TextUtils; +import android.util.SparseIntArray; import android.view.KeyCharacterMap; import com.android.inputmethod.latin.Constants; +import java.text.Normalizer; import java.util.ArrayList; import javax.annotation.Nonnull; @@ -29,9 +31,209 @@ import javax.annotation.Nonnull; * A combiner that handles dead keys. */ public class DeadKeyCombiner implements Combiner { + + private static class Data { + // This class data taken from KeyCharacterMap.java. + + /* Characters used to display placeholders for dead keys. */ + private static final int ACCENT_ACUTE = '\u00B4'; + private static final int ACCENT_BREVE = '\u02D8'; + private static final int ACCENT_CARON = '\u02C7'; + private static final int ACCENT_CEDILLA = '\u00B8'; + private static final int ACCENT_CIRCUMFLEX = '\u02C6'; + private static final int ACCENT_COMMA_ABOVE = '\u1FBD'; + private static final int ACCENT_COMMA_ABOVE_RIGHT = '\u02BC'; + private static final int ACCENT_DOT_ABOVE = '\u02D9'; + private static final int ACCENT_DOT_BELOW = Constants.CODE_PERIOD; // approximate + private static final int ACCENT_DOUBLE_ACUTE = '\u02DD'; + private static final int ACCENT_GRAVE = '\u02CB'; + private static final int ACCENT_HOOK_ABOVE = '\u02C0'; + private static final int ACCENT_HORN = Constants.CODE_SINGLE_QUOTE; // approximate + private static final int ACCENT_MACRON = '\u00AF'; + private static final int ACCENT_MACRON_BELOW = '\u02CD'; + private static final int ACCENT_OGONEK = '\u02DB'; + private static final int ACCENT_REVERSED_COMMA_ABOVE = '\u02BD'; + private static final int ACCENT_RING_ABOVE = '\u02DA'; + private static final int ACCENT_STROKE = Constants.CODE_DASH; // approximate + private static final int ACCENT_TILDE = '\u02DC'; + private static final int ACCENT_TURNED_COMMA_ABOVE = '\u02BB'; + private static final int ACCENT_UMLAUT = '\u00A8'; + private static final int ACCENT_VERTICAL_LINE_ABOVE = '\u02C8'; + private static final int ACCENT_VERTICAL_LINE_BELOW = '\u02CC'; + + /* Legacy dead key display characters used in previous versions of the API (before L) + * We still support these characters by mapping them to their non-legacy version. */ + private static final int ACCENT_GRAVE_LEGACY = Constants.CODE_GRAVE_ACCENT; + private static final int ACCENT_CIRCUMFLEX_LEGACY = Constants.CODE_CIRCUMFLEX_ACCENT; + private static final int ACCENT_TILDE_LEGACY = Constants.CODE_TILDE; + + /** + * Maps Unicode combining diacritical to display-form dead key. + */ + private static final SparseIntArray sCombiningToAccent = new SparseIntArray(); + private static final SparseIntArray sAccentToCombining = new SparseIntArray(); + static { + // U+0300: COMBINING GRAVE ACCENT + addCombining('\u0300', ACCENT_GRAVE); + // U+0301: COMBINING ACUTE ACCENT + addCombining('\u0301', ACCENT_ACUTE); + // U+0302: COMBINING CIRCUMFLEX ACCENT + addCombining('\u0302', ACCENT_CIRCUMFLEX); + // U+0303: COMBINING TILDE + addCombining('\u0303', ACCENT_TILDE); + // U+0304: COMBINING MACRON + addCombining('\u0304', ACCENT_MACRON); + // U+0306: COMBINING BREVE + addCombining('\u0306', ACCENT_BREVE); + // U+0307: COMBINING DOT ABOVE + addCombining('\u0307', ACCENT_DOT_ABOVE); + // U+0308: COMBINING DIAERESIS + addCombining('\u0308', ACCENT_UMLAUT); + // U+0309: COMBINING HOOK ABOVE + addCombining('\u0309', ACCENT_HOOK_ABOVE); + // U+030A: COMBINING RING ABOVE + addCombining('\u030A', ACCENT_RING_ABOVE); + // U+030B: COMBINING DOUBLE ACUTE ACCENT + addCombining('\u030B', ACCENT_DOUBLE_ACUTE); + // U+030C: COMBINING CARON + addCombining('\u030C', ACCENT_CARON); + // U+030D: COMBINING VERTICAL LINE ABOVE + addCombining('\u030D', ACCENT_VERTICAL_LINE_ABOVE); + // U+030E: COMBINING DOUBLE VERTICAL LINE ABOVE + //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE); + // U+030F: COMBINING DOUBLE GRAVE ACCENT + //addCombining('\u030F', ACCENT_DOUBLE_GRAVE); + // U+0310: COMBINING CANDRABINDU + //addCombining('\u0310', ACCENT_CANDRABINDU); + // U+0311: COMBINING INVERTED BREVE + //addCombining('\u0311', ACCENT_INVERTED_BREVE); + // U+0312: COMBINING TURNED COMMA ABOVE + addCombining('\u0312', ACCENT_TURNED_COMMA_ABOVE); + // U+0313: COMBINING COMMA ABOVE + addCombining('\u0313', ACCENT_COMMA_ABOVE); + // U+0314: COMBINING REVERSED COMMA ABOVE + addCombining('\u0314', ACCENT_REVERSED_COMMA_ABOVE); + // U+0315: COMBINING COMMA ABOVE RIGHT + addCombining('\u0315', ACCENT_COMMA_ABOVE_RIGHT); + // U+031B: COMBINING HORN + addCombining('\u031B', ACCENT_HORN); + // U+0323: COMBINING DOT BELOW + addCombining('\u0323', ACCENT_DOT_BELOW); + // U+0326: COMBINING COMMA BELOW + //addCombining('\u0326', ACCENT_COMMA_BELOW); + // U+0327: COMBINING CEDILLA + addCombining('\u0327', ACCENT_CEDILLA); + // U+0328: COMBINING OGONEK + addCombining('\u0328', ACCENT_OGONEK); + // U+0329: COMBINING VERTICAL LINE BELOW + addCombining('\u0329', ACCENT_VERTICAL_LINE_BELOW); + // U+0331: COMBINING MACRON BELOW + addCombining('\u0331', ACCENT_MACRON_BELOW); + // U+0335: COMBINING SHORT STROKE OVERLAY + addCombining('\u0335', ACCENT_STROKE); + // U+0342: COMBINING GREEK PERISPOMENI + //addCombining('\u0342', ACCENT_PERISPOMENI); + // U+0344: COMBINING GREEK DIALYTIKA TONOS + //addCombining('\u0344', ACCENT_DIALYTIKA_TONOS); + // U+0345: COMBINING GREEK YPOGEGRAMMENI + //addCombining('\u0345', ACCENT_YPOGEGRAMMENI); + + // One-way mappings to equivalent preferred accents. + // U+0340: COMBINING GRAVE TONE MARK + sCombiningToAccent.append('\u0340', ACCENT_GRAVE); + // U+0341: COMBINING ACUTE TONE MARK + sCombiningToAccent.append('\u0341', ACCENT_ACUTE); + // U+0343: COMBINING GREEK KORONIS + sCombiningToAccent.append('\u0343', ACCENT_COMMA_ABOVE); + + // One-way legacy mappings to preserve compatibility with older applications. + // U+0300: COMBINING GRAVE ACCENT + sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300'); + // U+0302: COMBINING CIRCUMFLEX ACCENT + sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302'); + // U+0303: COMBINING TILDE + sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303'); + } + + private static void addCombining(int combining, int accent) { + sCombiningToAccent.append(combining, accent); + sAccentToCombining.append(accent, combining); + } + + // Caution! This may only contain chars, not supplementary code points. It's unlikely + // it will ever need to, but if it does we'll have to change this + private static final SparseIntArray sNonstandardDeadCombinations = new SparseIntArray(); + static { + // Non-standard decompositions. + // Stroke modifier for Finnish multilingual keyboard and others. + // U+0110: LATIN CAPITAL LETTER D WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'D', '\u0110'); + // U+01E4: LATIN CAPITAL LETTER G WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'G', '\u01e4'); + // U+0126: LATIN CAPITAL LETTER H WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'H', '\u0126'); + // U+0197: LATIN CAPITAL LETTER I WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'I', '\u0197'); + // U+0141: LATIN CAPITAL LETTER L WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'L', '\u0141'); + // U+00D8: LATIN CAPITAL LETTER O WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'O', '\u00d8'); + // U+0166: LATIN CAPITAL LETTER T WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'T', '\u0166'); + // U+0111: LATIN SMALL LETTER D WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'd', '\u0111'); + // U+01E5: LATIN SMALL LETTER G WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'g', '\u01e5'); + // U+0127: LATIN SMALL LETTER H WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'h', '\u0127'); + // U+0268: LATIN SMALL LETTER I WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'i', '\u0268'); + // U+0142: LATIN SMALL LETTER L WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'l', '\u0142'); + // U+00F8: LATIN SMALL LETTER O WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'o', '\u00f8'); + // U+0167: LATIN SMALL LETTER T WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 't', '\u0167'); + } + + private static void addNonStandardDeadCombination(final int deadCodePoint, + final int spacingCodePoint, final int result) { + final int combination = (deadCodePoint << 16) | spacingCodePoint; + sNonstandardDeadCombinations.put(combination, result); + } + + public static final int NOT_A_CHAR = 0; + public static final int BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION = 16; + // Get a non-standard combination + public static char getNonstandardCombination(final int deadCodePoint, + final int spacingCodePoint) { + final int combination = spacingCodePoint | + (deadCodePoint << BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION); + return (char)sNonstandardDeadCombinations.get(combination, NOT_A_CHAR); + } + } + // TODO: make this a list of events instead final StringBuilder mDeadSequence = new StringBuilder(); + @Nonnull + private Event createEventChainFromSequence(final @Nonnull CharSequence text, + final Event originalEvent) { + if (text.length() <= 0) { + return originalEvent; + } else { + Event lastEvent = null; + int codePoint = 0; + for (int i = text.length(); i > 0; i -= Character.charCount(codePoint)) { + codePoint = Character.codePointBefore(text, i); + final Event thisEvent = Event.createHardwareKeypressEvent(codePoint, + originalEvent.mKeyCode, lastEvent, false /* isKeyRepeat */); + lastEvent = thisEvent; + } + return lastEvent; + } + } + @Override @Nonnull public Event processEvent(final ArrayList<Event> previousEvents, final Event event) { @@ -47,29 +249,48 @@ public class DeadKeyCombiner implements Combiner { // simply returns the event as is. The majority of events will go through this path. return event; } else { - // TODO: Allow combining for several dead chars rather than only the first one. - // The framework doesn't know how to do this now. - final int deadCodePoint = mDeadSequence.codePointAt(0); - mDeadSequence.setLength(0); - final int resultingCodePoint = - KeyCharacterMap.getDeadChar(deadCodePoint, event.mCodePoint); - if (0 == resultingCodePoint) { - // We can't combine both characters. We need to commit the dead key as a separate - // character, and the next char too unless it's a space (because as a special case, - // dead key + space should result in only the dead key being committed - that's - // how dead keys work). - // If the event is a space, we should commit the dead char alone, but if it's - // not, we need to commit both. - // TODO: this is not necessarily triggered by hardware key events, so it's not - // a good idea to masquerade as one. This should be typed as a software - // composite event or something. - return Event.createHardwareKeypressEvent(deadCodePoint, event.mKeyCode, - Constants.CODE_SPACE == event.mCodePoint ? null : event /* next */, - false /* isKeyRepeat */); + if (Character.isWhitespace(event.mCodePoint) + || event.mCodePoint == mDeadSequence.codePointBefore(mDeadSequence.length())) { + // When whitespace or twice the same dead key, we should output the dead sequence + // as is. + final Event resultEvent = createEventChainFromSequence(mDeadSequence.toString(), + event); + mDeadSequence.setLength(0); + return resultEvent; + } else if (event.isFunctionalKeyEvent()) { + if (Constants.CODE_DELETE == event.mKeyCode) { + // Remove the last code point + final int trimIndex = mDeadSequence.length() - Character.charCount( + mDeadSequence.codePointBefore(mDeadSequence.length())); + mDeadSequence.setLength(trimIndex); + return Event.createConsumedEvent(event); + } else { + return event; + } + } else if (event.isDead()) { + mDeadSequence.appendCodePoint(event.mCodePoint); + return Event.createConsumedEvent(event); } else { - // We could combine the characters. - return Event.createHardwareKeypressEvent(resultingCodePoint, event.mKeyCode, - null /* next */, false /* isKeyRepeat */); + // Combine normally. + final StringBuilder sb = new StringBuilder(); + sb.appendCodePoint(event.mCodePoint); + int codePointIndex = 0; + while (codePointIndex < mDeadSequence.length()) { + final int deadCodePoint = mDeadSequence.codePointAt(codePointIndex); + final char replacementSpacingChar = + Data.getNonstandardCombination(deadCodePoint, event.mCodePoint); + if (Data.NOT_A_CHAR != replacementSpacingChar) { + sb.setCharAt(0, replacementSpacingChar); + } else { + final int combining = Data.sAccentToCombining.get(deadCodePoint); + sb.appendCodePoint(0 == combining ? deadCodePoint : combining); + } + codePointIndex += Character.isSupplementaryCodePoint(deadCodePoint) ? 2 : 1; + } + final String normalizedString = Normalizer.normalize(sb, Normalizer.Form.NFC); + final Event resultEvent = createEventChainFromSequence(normalizedString, event); + mDeadSequence.setLength(0); + return resultEvent; } } } diff --git a/java/src/com/android/inputmethod/event/Event.java b/java/src/com/android/inputmethod/event/Event.java index ef5b04747..ff6f88066 100644 --- a/java/src/com/android/inputmethod/event/Event.java +++ b/java/src/com/android/inputmethod/event/Event.java @@ -16,6 +16,7 @@ package com.android.inputmethod.event; +import com.android.inputmethod.annotations.ExternallyReferenced; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.utils.StringUtils; @@ -147,6 +148,7 @@ public class Event { } // This creates an input event for a dead character. @see {@link #FLAG_DEAD} + @ExternallyReferenced public static Event createDeadEvent(final int codePoint, final int keyCode, final Event next) { // TODO: add an argument or something if we ever create a software layout with dead keys. return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 86ea4c563..bd1c1479a 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -94,13 +94,13 @@ public class Key implements Comparable<Key> { /** Icon to display instead of a label. Icon takes precedence over a label */ private final int mIconId; - /** Width of the key, not including the gap */ + /** Width of the key, excluding the gap */ private final int mWidth; - /** Height of the key, not including the gap */ + /** Height of the key, excluding the gap */ private final int mHeight; - /** X coordinate of the key in the keyboard layout */ + /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ private final int mX; - /** Y coordinate of the key in the keyboard layout */ + /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ private final int mY; /** Hit bounding box of the key */ private final Rect mHitBox = new Rect(); @@ -767,18 +767,34 @@ public class Key implements Comparable<Key> { return iconSet.getIconDrawable(getIconId()); } + /** + * Gets the width of the key in pixels, excluding the gap. + * @return The width of the key in pixels, excluding the gap. + */ public int getWidth() { return mWidth; } + /** + * Gets the height of the key in pixels, excluding the gap. + * @return The height of the key in pixels, excluding the gap. + */ public int getHeight() { return mHeight; } + /** + * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap. + * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap. + */ public int getX() { return mX; } + /** + * Gets the y-coordinate of the top-left corner of the key in pixels, excluding the gap. + * @return The y-coordinate of the top-left corner of the key in pixels, excluding the gap. + */ public int getY() { return mY; } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java index 3c1167538..538e515bc 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java @@ -21,9 +21,9 @@ import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOAR import android.text.InputType; import android.text.TextUtils; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.EditorInfoCompatUtils; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; @@ -62,7 +62,7 @@ public final class KeyboardId { public static final int ELEMENT_EMOJI_CATEGORY5 = 15; public static final int ELEMENT_EMOJI_CATEGORY6 = 16; - public final InputMethodSubtype mSubtype; + public final RichInputMethodSubtype mSubtype; public final Locale mLocale; public final int mWidth; public final int mHeight; diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index feb79efe9..3f4367313 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -28,7 +28,6 @@ import android.util.Log; import android.util.SparseArray; import android.util.Xml; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.EditorInfoCompatUtils; import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; @@ -37,6 +36,7 @@ import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.keyboard.internal.KeysCache; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.SubtypeSwitcher; import com.android.inputmethod.latin.define.DebugFlags; import com.android.inputmethod.latin.utils.InputTypeUtils; @@ -109,7 +109,7 @@ public final class KeyboardLayoutSet { boolean mVoiceInputKeyEnabled; boolean mNoSettingsKey; boolean mLanguageSwitchKeyEnabled; - InputMethodSubtype mSubtype; + RichInputMethodSubtype mSubtype; boolean mIsSpellChecker; int mKeyboardWidth; int mKeyboardHeight; @@ -253,7 +253,7 @@ public final class KeyboardLayoutSet { return this; } - public Builder setSubtype(final InputMethodSubtype subtype) { + public Builder setSubtype(final RichInputMethodSubtype subtype) { final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype); // TODO: Consolidate with {@link InputAttributes}. @SuppressWarnings("deprecation") @@ -262,7 +262,7 @@ public final class KeyboardLayoutSet { final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( mParams.mEditorInfo.imeOptions) || deprecatedForceAscii; - final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) + final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) ? SubtypeSwitcher.getInstance().getNoLanguageSubtype() : subtype; mParams.mSubtype = keyboardSubtype; diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index e4875164a..28f2dcf89 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -28,6 +28,7 @@ import android.view.View; import android.view.inputmethod.EditorInfo; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; +import com.android.inputmethod.event.Event; import com.android.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException; import com.android.inputmethod.keyboard.emoji.EmojiPalettesView; import com.android.inputmethod.keyboard.internal.KeyboardState; @@ -306,9 +307,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { /** * Updates state machine to figure out when to automatically switch back to the previous mode. */ - public void onCodeInput(final int code, final int currentAutoCapsState, + public void onEvent(final Event event, final int currentAutoCapsState, final int currentRecapitalizeState) { - mState.onCodeInput(code, currentAutoCapsState, currentRecapitalizeState); + mState.onEvent(event, currentAutoCapsState, currentRecapitalizeState); } public boolean isShowingEmojiPalettes() { diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 6d0b2c2f1..2ed4ea98e 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -34,7 +34,6 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.MainKeyboardAccessibilityDelegate; @@ -54,10 +53,10 @@ import com.android.inputmethod.keyboard.internal.SlidingKeyInputDrawingPreview; import com.android.inputmethod.keyboard.internal.TimerHandler; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.settings.DebugSettings; import com.android.inputmethod.latin.utils.CoordinateUtils; -import com.android.inputmethod.latin.utils.SpacebarLanguageUtils; import com.android.inputmethod.latin.utils.TypefaceUtils; import java.util.WeakHashMap; @@ -873,16 +872,16 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // Layout language name on spacebar. private String layoutLanguageOnSpacebar(final Paint paint, - final InputMethodSubtype subtype, final int width) { + final RichInputMethodSubtype subtype, final int width) { // Choose appropriate language name to fit into the width. if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarHelper.FORMAT_TYPE_FULL_LOCALE) { - final String fullText = SpacebarLanguageUtils.getFullDisplayName(subtype); + final String fullText = subtype.getFullDisplayName(); if (fitsTextIntoWidth(width, fullText, paint)) { return fullText; } } - final String middleText = SpacebarLanguageUtils.getMiddleDisplayName(subtype); + final String middleText = subtype.getMiddleDisplayName(); if (fitsTextIntoWidth(width, middleText, paint)) { return middleText; } @@ -896,7 +895,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack paint.setTextAlign(Align.CENTER); paint.setTypeface(Typeface.DEFAULT); paint.setTextSize(mLanguageOnSpacebarTextSize); - final InputMethodSubtype subtype = getKeyboard().mId.mSubtype; + final RichInputMethodSubtype subtype = getKeyboard().mId.mSubtype; final String language = layoutLanguageOnSpacebar(paint, subtype, width); // Draw language text with shadow final float descent = paint.descent(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java index b98ced97c..5f4d55bdb 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java @@ -19,6 +19,7 @@ package com.android.inputmethod.keyboard.internal; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.event.Event; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.RecapitalizeStatus; @@ -29,7 +30,7 @@ import com.android.inputmethod.latin.utils.RecapitalizeStatus; * * The input events are {@link #onLoadKeyboard(int, int)}, {@link #onSaveKeyboardState()}, * {@link #onPressKey(int,boolean,int,int)}, {@link #onReleaseKey(int,boolean,int,int)}, - * {@link #onCodeInput(int,int,int)}, {@link #onFinishSlidingInput(int,int)}, + * {@link #onEvent(Event,int,int)}, {@link #onFinishSlidingInput(int,int)}, * {@link #onUpdateShiftState(int,int)}, {@link #onResetKeyboardStateToAlphabet(int,int)}. * * The actions are {@link SwitchActions}'s methods. @@ -610,10 +611,11 @@ public final class KeyboardState { return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER; } - public void onCodeInput(final int code, final int currentAutoCapsState, + public void onEvent(final Event event, final int currentAutoCapsState, final int currentRecapitalizeState) { + final int code = event.isFunctionalKeyEvent() ? event.mKeyCode : event.mCodePoint; if (DEBUG_EVENT) { - Log.d(TAG, "onCodeInput: code=" + Constants.printableCode(code) + Log.d(TAG, "onEvent: code=" + Constants.printableCode(code) + " autoCaps=" + currentAutoCapsState + " " + this); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java index 31bc549ca..0e3acff84 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java @@ -94,7 +94,7 @@ public final class KeyboardTextsTable { /* 8:22 */ "morekeys_n", /* 9:22 */ "single_quotes", /* 10:20 */ "morekeys_s", - /* 11:17 */ "keyspec_currency", + /* 11:18 */ "keyspec_currency", /* 12:14 */ "morekeys_y", /* 13:13 */ "morekeys_d", /* 14:12 */ "morekeys_z", @@ -1874,6 +1874,15 @@ public final class KeyboardTextsTable { /* additional_morekeys_symbols_0 */ "0", }; + /* Locale hi_ZZ: Hindi (ZZ) */ + private static final String[] TEXTS_hi_ZZ = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_s */ + // U+20B9: "₹" INDIAN RUPEE SIGN + /* keyspec_currency */ "\u20B9", + }; + /* Locale hr: Croatian */ private static final String[] TEXTS_hr = { /* morekeys_a ~ */ @@ -3957,6 +3966,7 @@ public final class KeyboardTextsTable { "fr" , TEXTS_fr, /* 13/ 62 French */ "gl_ES" , TEXTS_gl_ES, /* 7/ 9 Gallegan (Spain) */ "hi" , TEXTS_hi, /* 23/ 53 Hindi */ + "hi_ZZ" , TEXTS_hi_ZZ, /* 1/ 12 Hindi (ZZ) */ "hr" , TEXTS_hr, /* 9/ 20 Croatian */ "hu" , TEXTS_hu, /* 9/ 20 Hungarian */ "hy_AM" , TEXTS_hy_AM, /* 9/126 Armenian (Armenia) */ diff --git a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java index 6400a2440..72ad2bd97 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java +++ b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java @@ -18,6 +18,7 @@ package com.android.inputmethod.keyboard.internal; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.util.Collections; @@ -34,8 +35,8 @@ public final class LanguageOnSpacebarHelper { private List<InputMethodSubtype> mEnabledSubtypes = Collections.emptyList(); private boolean mIsSystemLanguageSameAsInputLanguage; - public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { - if (SubtypeLocaleUtils.isNoLanguage(subtype)) { + public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) { + if (subtype.isNoLanguage()) { return FORMAT_TYPE_FULL_LOCALE; } // Only this subtype is enabled and equals to the system locale. diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 693e1cdcc..2e108756e 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -83,7 +83,6 @@ public final class BinaryDictionary extends Dictionary { public static final String DIR_NAME_SUFFIX_FOR_RECORD_MIGRATION = ".migrating"; private long mNativeDict; - private final Locale mLocale; private final long mDictSize; private final String mDictFilePath; private final boolean mUseFullEditDistance; @@ -117,8 +116,7 @@ public final class BinaryDictionary extends Dictionary { public BinaryDictionary(final String filename, final long offset, final long length, final boolean useFullEditDistance, final Locale locale, final String dictType, final boolean isUpdatable) { - super(dictType); - mLocale = locale; + super(dictType, locale); mDictSize = length; mDictFilePath = filename; mIsUpdatable = isUpdatable; @@ -138,8 +136,7 @@ public final class BinaryDictionary extends Dictionary { public BinaryDictionary(final String filename, final boolean useFullEditDistance, final Locale locale, final String dictType, final long formatVersion, final Map<String, String> attributeMap) { - super(dictType); - mLocale = locale; + super(dictType, locale); mDictSize = 0; mDictFilePath = filename; // On memory dictionary is always updatable. diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 43af66eb7..fc7f95c7b 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -57,6 +57,13 @@ public final class Constants { @SuppressWarnings("dep-ann") public static final String FORCE_ASCII = "forceAscii"; + /** + * The private IME option used to suppress the floating gesture preview for a given text + * field. This overrides the corresponding keyboard settings preference. + * {@link com.android.inputmethod.latin.settings.SettingsValues#mGestureFloatingPreviewTextEnabled} + */ + public static final String NO_FLOATING_GESTURE_PREVIEW = "noGestureFloatingPreview"; + private ImeOption() { // This utility class is not publicly instantiable. } @@ -217,10 +224,12 @@ public final class Constants { public static final int CODE_CLOSING_ANGLE_BRACKET = '>'; public static final int CODE_INVERTED_QUESTION_MARK = 0xBF; // ¿ public static final int CODE_INVERTED_EXCLAMATION_MARK = 0xA1; // ¡ + public static final int CODE_GRAVE_ACCENT = '`'; + public static final int CODE_CIRCUMFLEX_ACCENT = '^'; + public static final int CODE_TILDE = '~'; public static final String REGEXP_PERIOD = "\\."; public static final String STRING_SPACE = " "; - public static final String STRING_PERIOD_AND_SPACE = ". "; /** * Special keys code. Must be negative. diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 560ced9c4..2f79c7662 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -16,12 +16,12 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import java.util.ArrayList; +import java.util.Locale; /** * Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key @@ -62,9 +62,12 @@ public abstract class Dictionary { // Contextual dictionary. public static final String TYPE_CONTEXTUAL = "contextual"; public final String mDictType; + // The locale for this dictionary. May be null if unknown (phony dictionary for example). + public final Locale mLocale; - public Dictionary(final String dictType) { + public Dictionary(final String dictType, final Locale locale) { mDictType = dictType; + mLocale = locale; } /** @@ -162,7 +165,7 @@ public abstract class Dictionary { private static class PhonyDictionary extends Dictionary { // This class is not publicly instantiable. private PhonyDictionary(final String type) { - super(type); + super(type, null); } @Override diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java index 2b4c54d48..ca5e93714 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java +++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java @@ -25,6 +25,7 @@ import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -34,13 +35,14 @@ public final class DictionaryCollection extends Dictionary { private final String TAG = DictionaryCollection.class.getSimpleName(); protected final CopyOnWriteArrayList<Dictionary> mDictionaries; - public DictionaryCollection(final String dictType) { - super(dictType); + public DictionaryCollection(final String dictType, final Locale locale) { + super(dictType, locale); mDictionaries = new CopyOnWriteArrayList<>(); } - public DictionaryCollection(final String dictType, final Dictionary... dictionaries) { - super(dictType); + public DictionaryCollection(final String dictType, final Locale locale, + final Dictionary... dictionaries) { + super(dictType, locale); if (null == dictionaries) { mDictionaries = new CopyOnWriteArrayList<>(); } else { @@ -49,8 +51,9 @@ public final class DictionaryCollection extends Dictionary { } } - public DictionaryCollection(final String dictType, final Collection<Dictionary> dictionaries) { - super(dictType); + public DictionaryCollection(final String dictType, final Locale locale, + final Collection<Dictionary> dictionaries) { + super(dictType, locale); mDictionaries = new CopyOnWriteArrayList<>(dictionaries); mDictionaries.removeAll(Collections.singleton(null)); } diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index 36a02669d..c20546607 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -19,10 +19,12 @@ package com.android.inputmethod.latin; import android.content.Context; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback; import com.android.inputmethod.latin.PrevWordsInfo.WordInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.personalization.ContextualDictionary; @@ -32,9 +34,9 @@ import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.DistracterFilter; +import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; import com.android.inputmethod.latin.utils.ExecutorUtils; -import com.android.inputmethod.latin.utils.LanguageModelParam; import com.android.inputmethod.latin.utils.SuggestionResults; import java.io.File; @@ -59,12 +61,13 @@ public class DictionaryFacilitator { // dictionary. private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; - private Dictionaries mDictionaries = new Dictionaries(); + private DictionaryGroup mDictionaryGroup = new DictionaryGroup(); private boolean mIsUserDictEnabled = false; private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0); - // To synchronize assigning mDictionaries to ensure closing dictionaries. + // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. private final Object mLock = new Object(); private final DistracterFilter mDistracterFilter; + private final PersonalizationHelperForDictionaryFacilitator mPersonalizationHelper; private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = new String[] { @@ -96,22 +99,22 @@ public class DictionaryFacilitator { DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); /** - * Class contains dictionaries for a locale. + * A group of dictionaries that work together for a single language. */ - private static class Dictionaries { + private static class DictionaryGroup { public final Locale mLocale; private Dictionary mMainDict; public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = new ConcurrentHashMap<>(); - public Dictionaries() { + public DictionaryGroup() { mLocale = null; } - public Dictionaries(final Locale locale, final Dictionary mainDict, + public DictionaryGroup(final Locale locale, final Dictionary mainDict, final Map<String, ExpandableBinaryDictionary> subDicts) { mLocale = locale; - // Main dictionary can be asynchronously loaded. + // The main dictionary can be asynchronously loaded. setMainDict(mainDict); for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { setSubDict(entry.getKey(), entry.getValue()); @@ -172,18 +175,26 @@ public class DictionaryFacilitator { public DictionaryFacilitator() { mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; + mPersonalizationHelper = null; } - public DictionaryFacilitator(final DistracterFilter distracterFilter) { - mDistracterFilter = distracterFilter; + public DictionaryFacilitator(final Context context) { + mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context); + mPersonalizationHelper = + new PersonalizationHelperForDictionaryFacilitator(context, mDistracterFilter); } public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); + mPersonalizationHelper.updateEnabledSubtypes(enabledSubtypes); + } + + public void setIsMonolingualUser(final boolean isMonolingualUser) { + mPersonalizationHelper.setIsMonolingualUser(isMonolingualUser); } public Locale getLocale() { - return mDictionaries.mLocale; + return mDictionaryGroup.mLocale; } private static ExpandableBinaryDictionary getSubDict(final String dictType, @@ -215,93 +226,125 @@ public class DictionaryFacilitator { usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */); } - public void resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale, + public void resetDictionariesWithDictNamePrefix(final Context context, + final Locale newLocaleToUse, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener, final String dictNamePrefix) { - final boolean localeHasBeenChanged = !newLocale.equals(mDictionaries.mLocale); - // We always try to have the main dictionary. Other dictionaries can be unused. - final boolean reloadMainDictionary = localeHasBeenChanged || forceReloadMainDictionary; + final HashMap<Locale, ArrayList<String>> existingDictsToCleanup = new HashMap<>(); + // TODO: use several locales + final Locale[] newLocales = new Locale[] { newLocaleToUse }; // TODO: Make subDictTypesToUse configurable by resource or a static final list. final HashSet<String> subDictTypesToUse = new HashSet<>(); + subDictTypesToUse.add(Dictionary.TYPE_USER); if (useContactsDict) { subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); } - subDictTypesToUse.add(Dictionary.TYPE_USER); if (usePersonalizedDicts) { subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); } - final Dictionary newMainDict; - if (reloadMainDictionary) { - // The main dictionary will be asynchronously loaded. - newMainDict = null; - } else { - newMainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); - } - - final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); - for (final String dictType : SUB_DICT_TYPES) { - if (!subDictTypesToUse.contains(dictType)) { - // This dictionary will not be used. + // Gather all dictionaries. We'll remove them from the list to clean up later. + for (final Locale newLocale : newLocales) { + final ArrayList<String> dictsForLocale = new ArrayList<>(); + existingDictsToCleanup.put(newLocale, dictsForLocale); + final DictionaryGroup currentDictionaryGroupForLocale = + newLocale.equals(mDictionaryGroup.mLocale) ? mDictionaryGroup : null; + if (null == currentDictionaryGroupForLocale) { continue; } - final ExpandableBinaryDictionary dict; - if (!localeHasBeenChanged && mDictionaries.hasDict(dictType)) { - // Continue to use current dictionary. - dict = mDictionaries.getSubDict(dictType); + for (final String dictType : SUB_DICT_TYPES) { + if (currentDictionaryGroupForLocale.hasDict(dictType)) { + dictsForLocale.add(dictType); + } + } + if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) { + dictsForLocale.add(Dictionary.TYPE_MAIN); + } + } + + final HashMap<Locale, DictionaryGroup> newDictionaryGroups = new HashMap<>(); + for (final Locale newLocale : newLocales) { + final DictionaryGroup dictionaryGroupForLocale = + newLocale.equals(mDictionaryGroup.mLocale) ? mDictionaryGroup : null; + final ArrayList<String> dictsToCleanupForLocale = existingDictsToCleanup.get(newLocale); + final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); + + final Dictionary mainDict; + if (forceReloadMainDictionary || noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) { + mainDict = null; } else { - // Start to use new dictionary. - dict = getSubDict(dictType, context, newLocale, null /* dictFile */, - dictNamePrefix); + mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); + dictsToCleanupForLocale.remove(Dictionary.TYPE_MAIN); + } + + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + for (final String subDictType : subDictTypesToUse) { + final ExpandableBinaryDictionary subDict; + if (noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(subDictType)) { + // Create a new dictionary. + subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, + dictNamePrefix); + } else { + // Reuse the existing dictionary, and don't close it at the end + subDict = dictionaryGroupForLocale.getSubDict(subDictType); + dictsToCleanupForLocale.remove(subDictType); + } + subDicts.put(subDictType, subDict); } - subDicts.put(dictType, dict); + newDictionaryGroups.put(newLocale, new DictionaryGroup(newLocale, mainDict, subDicts)); } // Replace Dictionaries. - final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, subDicts); - final Dictionaries oldDictionaries; + // TODO: use multiple locales. + final DictionaryGroup newDictionaryGroup = newDictionaryGroups.get(newLocaleToUse); + final DictionaryGroup oldDictionaryGroup; synchronized (mLock) { - oldDictionaries = mDictionaries; - mDictionaries = newDictionaries; + oldDictionaryGroup = mDictionaryGroup; + mDictionaryGroup = newDictionaryGroup; mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); - if (reloadMainDictionary) { - asyncReloadMainDictionary(context, newLocale, listener); + if (null == newDictionaryGroup.getDict(Dictionary.TYPE_MAIN)) { + asyncReloadUninitializedMainDictionaries(context, newLocales, listener); } } if (listener != null) { listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); } + // Clean up old dictionaries. - if (reloadMainDictionary) { - oldDictionaries.closeDict(Dictionary.TYPE_MAIN); - } - for (final String dictType : SUB_DICT_TYPES) { - if (localeHasBeenChanged || !subDictTypesToUse.contains(dictType)) { - oldDictionaries.closeDict(dictType); + for (final Locale localeToCleanUp : existingDictsToCleanup.keySet()) { + final ArrayList<String> dictTypesToCleanUp = + existingDictsToCleanup.get(localeToCleanUp); + final DictionaryGroup dictionarySetToCleanup = oldDictionaryGroup; + for (final String dictType : dictTypesToCleanUp) { + dictionarySetToCleanup.closeDict(dictType); } } - oldDictionaries.mSubDictMap.clear(); } - private void asyncReloadMainDictionary(final Context context, final Locale locale, - final DictionaryInitializationListener listener) { + private void asyncReloadUninitializedMainDictionaries(final Context context, + final Locale[] locales, final DictionaryInitializationListener listener) { final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); mLatchForWaitingLoadingMainDictionary = latchForWaitingLoadingMainDictionary; ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { @Override public void run() { - final Dictionary mainDict = - DictionaryFactory.createMainDictionaryFromManager(context, locale); - synchronized (mLock) { - if (locale.equals(mDictionaries.mLocale)) { - mDictionaries.setMainDict(mainDict); - } else { - // Dictionary facilitator has been reset for another locale. - mainDict.close(); + for (final Locale locale : locales) { + final DictionaryGroup dictionaryGroup = mDictionaryGroup; + final Dictionary mainDict = + DictionaryFactory.createMainDictionaryFromManager(context, locale); + synchronized (mLock) { + if (locale.equals(dictionaryGroup.mLocale)) { + dictionaryGroup.setMainDict(mainDict); + } else { + // Dictionary facilitator has been reset for another locale. + mainDict.close(); + } } } if (listener != null) { @@ -338,43 +381,46 @@ public class DictionaryFacilitator { subDicts.put(dictType, dict); } } - mDictionaries = new Dictionaries(locale, mainDictionary, subDicts); + mDictionaryGroup = new DictionaryGroup(locale, mainDictionary, subDicts); } public void closeDictionaries() { - final Dictionaries dictionaries; + final DictionaryGroup dictionaryGroup; synchronized (mLock) { - dictionaries = mDictionaries; - mDictionaries = new Dictionaries(); + dictionaryGroup = mDictionaryGroup; + mDictionaryGroup = new DictionaryGroup(); } for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - dictionaries.closeDict(dictType); + dictionaryGroup.closeDict(dictType); } mDistracterFilter.close(); + if (mPersonalizationHelper != null) { + mPersonalizationHelper.close(); + } } @UsedForTesting public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { - return mDictionaries.getSubDict(dictName); + return mDictionaryGroup.getSubDict(dictName); } // The main dictionary could have been loaded asynchronously. Don't cache the return value // of this method. public boolean hasInitializedMainDictionary() { - final Dictionary mainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); + final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); return mainDict != null && mainDict.isInitialized(); } public boolean hasPersonalizationDictionary() { - return mDictionaries.hasDict(Dictionary.TYPE_PERSONALIZATION); + return mDictionaryGroup.hasDict(Dictionary.TYPE_PERSONALIZATION); } public void flushPersonalizationDictionary() { - final ExpandableBinaryDictionary personalizationDict = - mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); - if (personalizationDict != null) { - personalizationDict.asyncFlushBinaryDictionary(); - } + final ExpandableBinaryDictionary personalizationDictUsedForSuggestion = + mDictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION); + mPersonalizationHelper.flushPersonalizationDictionariesToUpdate( + personalizationDictUsedForSuggestion); + mDistracterFilter.close(); } public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit) @@ -386,7 +432,7 @@ public class DictionaryFacilitator { public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) throws InterruptedException { waitForLoadingMainDictionary(timeout, unit); - final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaries.mSubDictMap; + final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaryGroup.mSubDictMap; for (final ExpandableBinaryDictionary dict : dictMap.values()) { dict.waitAllTasksForTests(); } @@ -407,24 +453,24 @@ public class DictionaryFacilitator { public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup dictionaryGroup = mDictionaryGroup; final String[] words = suggestion.split(Constants.WORD_SEPARATOR); PrevWordsInfo prevWordsInfoForCurrentWord = prevWordsInfo; for (int i = 0; i < words.length; i++) { final String currentWord = words[i]; final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; - addWordToUserHistory(dictionaries, prevWordsInfoForCurrentWord, currentWord, + addWordToUserHistory(dictionaryGroup, prevWordsInfoForCurrentWord, currentWord, wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); prevWordsInfoForCurrentWord = prevWordsInfoForCurrentWord.getNextPrevWordsInfo(new WordInfo(currentWord)); } } - private void addWordToUserHistory(final Dictionaries dictionaries, + private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { final ExpandableBinaryDictionary userHistoryDictionary = - dictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY); + dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY); if (userHistoryDictionary == null) { return; } @@ -432,7 +478,7 @@ public class DictionaryFacilitator { if (maxFreq == 0 && blockPotentiallyOffensive) { return; } - final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); final String secondWord; if (wasAutoCapitalized) { if (isValidWord(word, false /* ignoreCase */) @@ -453,8 +499,8 @@ public class DictionaryFacilitator { // History dictionary in order to avoid suggesting them until the dictionary // consolidation is done. // TODO: Remove this hack when ready. - final int lowerCaseFreqInMainDict = dictionaries.hasDict(Dictionary.TYPE_MAIN) ? - dictionaries.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : + final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN) ? + dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : Dictionary.NOT_A_PROBABILITY; if (maxFreq < lowerCaseFreqInMainDict && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { @@ -474,7 +520,7 @@ public class DictionaryFacilitator { } private void removeWord(final String dictName, final String word) { - final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); if (dictionary != null) { dictionary.removeUnigramEntryDynamically(word); } @@ -490,12 +536,12 @@ public class DictionaryFacilitator { public SuggestionResults getSuggestionResults(final WordComposer composer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup dictionaryGroup = mDictionaryGroup; final SuggestionResults suggestionResults = - new SuggestionResults(dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS); + new SuggestionResults(SuggestedWords.MAX_SUGGESTIONS); final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT }; for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaries.getDict(dictType); + final Dictionary dictionary = dictionaryGroup.getDict(dictType); if (null == dictionary) continue; final ArrayList<SuggestedWordInfo> dictionarySuggestions = dictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, @@ -513,13 +559,13 @@ public class DictionaryFacilitator { if (TextUtils.isEmpty(word)) { return false; } - final Dictionaries dictionaries = mDictionaries; - if (dictionaries.mLocale == null) { + final DictionaryGroup dictionaryGroup = mDictionaryGroup; + if (dictionaryGroup.mLocale == null) { return false; } - final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaries.getDict(dictType); + final Dictionary dictionary = dictionaryGroup.getDict(dictType); // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and // would be immutable once it's finished initializing, but concretely a null test is // probably good enough for the time being. @@ -538,9 +584,9 @@ public class DictionaryFacilitator { return Dictionary.NOT_A_PROBABILITY; } int maxFreq = Dictionary.NOT_A_PROBABILITY; - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup dictionaryGroup = mDictionaryGroup; for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaries.getDict(dictType); + final Dictionary dictionary = dictionaryGroup.getDict(dictType); if (dictionary == null) continue; final int tempFreq; if (isGettingMaxFrequencyOfExactMatches) { @@ -564,7 +610,7 @@ public class DictionaryFacilitator { } private void clearSubDictionary(final String dictName) { - final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); if (dictionary != null) { dictionary.clear(); } @@ -578,6 +624,7 @@ public class DictionaryFacilitator { // personalization dictionary. public void clearPersonalizationDictionary() { clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); + mPersonalizationHelper.clearDictionariesToUpdate(); } public void clearContextualDictionary() { @@ -587,35 +634,15 @@ public class DictionaryFacilitator { public void addEntriesToPersonalizationDictionary( final PersonalizationDataChunk personalizationDataChunk, final SpacingAndPunctuations spacingAndPunctuations, - final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) { - final ExpandableBinaryDictionary personalizationDict = - mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); - if (personalizationDict == null) { - if (callback != null) { - callback.onFinished(); - } - return; - } - final ArrayList<LanguageModelParam> languageModelParams = - LanguageModelParam.createLanguageModelParamsFrom( - personalizationDataChunk.mTokens, - personalizationDataChunk.mTimestampInSeconds, - this /* dictionaryFacilitator */, spacingAndPunctuations, - new DistracterFilterCheckingIsInDictionary( - mDistracterFilter, personalizationDict)); - if (languageModelParams == null || languageModelParams.isEmpty()) { - if (callback != null) { - callback.onFinished(); - } - return; - } - personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback); + final AddMultipleDictionaryEntriesCallback callback) { + mPersonalizationHelper.addEntriesToPersonalizationDictionariesToUpdate( + getLocale(), personalizationDataChunk, spacingAndPunctuations, callback); } public void addPhraseToContextualDictionary(final String[] phrase, final int probability, final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { final ExpandableBinaryDictionary contextualDict = - mDictionaries.getSubDict(Dictionary.TYPE_CONTEXTUAL); + mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTEXTUAL); if (contextualDict == null) { return; } @@ -648,7 +675,7 @@ public class DictionaryFacilitator { } public void dumpDictionaryForDebug(final String dictName) { - final ExpandableBinaryDictionary dictToDump = mDictionaries.getSubDict(dictName); + final ExpandableBinaryDictionary dictToDump = mDictionaryGroup.getSubDict(dictName); if (dictToDump == null) { Log.e(TAG, "Cannot dump " + dictName + ". " + "The dictionary is not being used for suggestion or cannot be dumped."); @@ -656,4 +683,15 @@ public class DictionaryFacilitator { } dictToDump.dumpAllWordsForDebug(); } + + public ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts() { + final ArrayList<Pair<String, DictionaryStats>> statsOfEnabledSubDicts = new ArrayList<>(); + final DictionaryGroup dictionaryGroup = mDictionaryGroup; + for (final String dictType : SUB_DICT_TYPES) { + final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictType); + if (dictionary == null) continue; + statsOfEnabledSubDicts.add(new Pair<>(dictType, dictionary.getDictionaryStats())); + } + return statsOfEnabledSubDicts; + } } diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java new file mode 100644 index 000000000..fa0265d86 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import java.util.HashSet; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import com.android.inputmethod.annotations.UsedForTesting; + +import android.content.Context; +import android.util.Log; +import android.util.LruCache; + +/** + * Cache for dictionary facilitators of multiple locales. + * This class automatically creates and releases facilitator instances using LRU policy. + */ +public class DictionaryFacilitatorLruCache { + private static final String TAG = DictionaryFacilitatorLruCache.class.getSimpleName(); + private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000; + private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5; + + /** + * Class extends LruCache. This class tracks cached locales and closes evicted dictionaries by + * overriding entryRemoved. + */ + private static class DictionaryFacilitatorLruCacheInner extends + LruCache<Locale, DictionaryFacilitator> { + private final HashSet<Locale> mCachedLocales; + public DictionaryFacilitatorLruCacheInner(final HashSet<Locale> cachedLocales, + final int maxSize) { + super(maxSize); + mCachedLocales = cachedLocales; + } + + @Override + protected void entryRemoved(boolean evicted, Locale key, + DictionaryFacilitator oldValue, DictionaryFacilitator newValue) { + if (oldValue != null && oldValue != newValue) { + oldValue.closeDictionaries(); + } + if (key != null && newValue == null) { + // Remove locale from the cache when the dictionary facilitator for the locale is + // evicted and new facilitator is not set for the locale. + mCachedLocales.remove(key); + if (size() >= maxSize()) { + Log.w(TAG, "DictionaryFacilitator for " + key.toString() + + " has been evicted due to cache size limit." + + " size: " + size() + ", maxSize: " + maxSize()); + } + } + } + } + + private final Context mContext; + private final HashSet<Locale> mCachedLocales = new HashSet<>(); + private final String mDictionaryNamePrefix; + private final DictionaryFacilitatorLruCacheInner mLruCache; + private final Object mLock = new Object(); + private boolean mUseContactsDictionary = false; + + public DictionaryFacilitatorLruCache(final Context context, final int maxSize, + final String dictionaryNamePrefix) { + mContext = context; + mLruCache = new DictionaryFacilitatorLruCacheInner(mCachedLocales, maxSize); + mDictionaryNamePrefix = dictionaryNamePrefix; + } + + private void waitForLoadingMainDictionary(final DictionaryFacilitator dictionaryFacilitator) { + for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) { + try { + dictionaryFacilitator.waitForLoadingMainDictionary( + WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS); + return; + } catch (final InterruptedException e) { + Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e); + if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) { + Log.i(TAG, "Retry", e); + } else { + Log.w(TAG, "Give up retrying. Retried " + + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e); + } + } + } + } + + private void resetDictionariesForLocaleLocked(final DictionaryFacilitator dictionaryFacilitator, + final Locale locale) { + dictionaryFacilitator.resetDictionariesWithDictNamePrefix(mContext, locale, + mUseContactsDictionary, false /* usePersonalizedDicts */, + false /* forceReloadMainDictionary */, null /* listener */, + mDictionaryNamePrefix); + } + + public void setUseContactsDictionary(final boolean useContectsDictionary) { + if (mUseContactsDictionary == useContectsDictionary) { + // The value has not been changed. + return; + } + synchronized (mLock) { + mUseContactsDictionary = useContectsDictionary; + for (final Locale locale : mCachedLocales) { + final DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale); + resetDictionariesForLocaleLocked(dictionaryFacilitator, locale); + waitForLoadingMainDictionary(dictionaryFacilitator); + } + } + } + + public DictionaryFacilitator get(final Locale locale) { + DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale); + if (dictionaryFacilitator != null) { + // dictionary falicitator for the locale is in the cache. + return dictionaryFacilitator; + } + synchronized (mLock) { + dictionaryFacilitator = mLruCache.get(locale); + if (dictionaryFacilitator != null) { + return dictionaryFacilitator; + } + dictionaryFacilitator = new DictionaryFacilitator(); + resetDictionariesForLocaleLocked(dictionaryFacilitator, locale); + waitForLoadingMainDictionary(dictionaryFacilitator); + mLruCache.put(locale, dictionaryFacilitator); + mCachedLocales.add(locale); + return dictionaryFacilitator; + } + } + + public void evictAll() { + synchronized (mLock) { + mLruCache.evictAll(); + mCachedLocales.clear(); + } + } + + @UsedForTesting + HashSet<Locale> getCachedLocalesForTesting() { + return mCachedLocales; + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java index 59de4f82a..3459b426d 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java @@ -50,7 +50,7 @@ public final class DictionaryFactory { final Locale locale, final boolean useFullEditDistance) { if (null == locale) { Log.e(TAG, "No locale defined for dictionary"); - return new DictionaryCollection(Dictionary.TYPE_MAIN, + return new DictionaryCollection(Dictionary.TYPE_MAIN, locale, createReadOnlyBinaryDictionary(context, locale)); } @@ -75,7 +75,7 @@ public final class DictionaryFactory { // If the list is empty, that means we should not use any dictionary (for example, the user // explicitly disabled the main dictionary), so the following is okay. dictList is never // null, but if for some reason it is, DictionaryCollection handles it gracefully. - return new DictionaryCollection(Dictionary.TYPE_MAIN, dictList); + return new DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList); } /** @@ -188,7 +188,7 @@ public final class DictionaryFactory { public static Dictionary createDictionaryForTest(final AssetFileAddress[] dictionaryList, final boolean useFullEditDistance, Locale locale) { final DictionaryCollection dictionaryCollection = - new DictionaryCollection(Dictionary.TYPE_MAIN); + new DictionaryCollection(Dictionary.TYPE_MAIN, locale); for (final AssetFileAddress address : dictionaryList) { final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = new ReadOnlyBinaryDictionary( address.mFilename, address.mOffset, address.mLength, useFullEditDistance, diff --git a/java/src/com/android/inputmethod/latin/DictionaryStats.java b/java/src/com/android/inputmethod/latin/DictionaryStats.java new file mode 100644 index 000000000..75aa2411d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryStats.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import java.io.File; +import java.util.Locale; + +public class DictionaryStats { + public final Locale mLocale; + public final String mDictName; + public final String mDictFilePath; + public final long mDictFileSize; + // TODO: Add more members. + + public DictionaryStats(final Locale locale, final String dictName, final File dictFile) { + mLocale = locale; + mDictName = dictName; + mDictFilePath = dictFile.getAbsolutePath(); + mDictFileSize = dictFile.length(); + } +} diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index c11a220a4..a1dd67f27 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -86,9 +86,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ private final String mDictName; - /** Dictionary locale */ - private final Locale mLocale; - /** Dictionary file */ private final File mDictFile; @@ -137,10 +134,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ public ExpandableBinaryDictionary(final Context context, final String dictName, final Locale locale, final String dictType, final File dictFile) { - super(dictType); + super(dictType, locale); mDictName = dictName; mContext = context; - mLocale = locale; mDictFile = getDictFile(context, dictName, dictFile); mBinaryDictionary = null; mIsReloading = new AtomicBoolean(); @@ -644,10 +640,21 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { }); } + public DictionaryStats getDictionaryStats() { + reloadDictionaryIfRequired(); + mLock.readLock().lock(); + try { + // TODO: Get stats form the dictionary. + return new DictionaryStats(mLocale, mDictName, mDictFile); + } finally { + mLock.readLock().unlock(); + } + } + @UsedForTesting public void waitAllTasksForTests() { final CountDownLatch countDownLatch = new CountDownLatch(1); - ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { countDownLatch.countDown(); diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java index 9df3a2a9d..50a6e48e3 100644 --- a/java/src/com/android/inputmethod/latin/InputAttributes.java +++ b/java/src/com/android/inputmethod/latin/InputAttributes.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import static com.android.inputmethod.latin.Constants.ImeOption.NO_FLOATING_GESTURE_PREVIEW; import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; @@ -42,6 +43,12 @@ public final class InputAttributes { final public boolean mApplicationSpecifiedCompletionOn; final public boolean mShouldInsertSpacesAutomatically; final public boolean mShouldShowVoiceInputKey; + /** + * Whether the floating gesture preview should be disabled. If true, this should override the + * corresponding keyboard settings preference, always suppressing the floating preview text. + * {@link com.android.inputmethod.latin.settings.SettingsValues#mGestureFloatingPreviewTextEnabled} + */ + final public boolean mDisableGestureFloatingPreviewText; final public boolean mIsGeneralTextInput; final private int mInputType; final private EditorInfo mEditorInfo; @@ -77,6 +84,7 @@ public final class InputAttributes { mApplicationSpecifiedCompletionOn = false; mShouldInsertSpacesAutomatically = false; mShouldShowVoiceInputKey = false; + mDisableGestureFloatingPreviewText = false; mIsGeneralTextInput = false; return; } @@ -109,6 +117,9 @@ public final class InputAttributes { || hasNoMicrophoneKeyOption(); mShouldShowVoiceInputKey = !noMicrophone; + mDisableGestureFloatingPreviewText = InputAttributes.inPrivateImeOptions( + mPackageNameForPrivateImeOptions, NO_FLOATING_GESTURE_PREVIEW, editorInfo); + // If it's a browser edit field and auto correct is not ON explicitly, then // disable auto correction, but keep suggestions on. // If NO_SUGGESTIONS is set, don't do prediction. diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index 790e0d830..d57a881c0 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -145,6 +145,12 @@ public final class InputPointers { return mPointerIds.getPrimitiveArray(); } + /** + * Gets the time each point was registered, in milliseconds, relative to the first event in the + * sequence. + * @return The time each point was registered, in milliseconds, relative to the first event in + * the sequence. + */ public int[] getTimes() { if (DebugFlags.DEBUG_ENABLED || DEBUG_TIME) { if (!isValidTimeStamps()) { diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 86fe6429b..aac6276bc 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -96,6 +96,7 @@ import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.JniUtils; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; import com.android.inputmethod.latin.utils.StatsUtils; +import com.android.inputmethod.latin.utils.StatsUtilsManager; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.latin.utils.ViewLayoutUtils; @@ -133,8 +134,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private final Settings mSettings; private final DictionaryFacilitator mDictionaryFacilitator = - new DictionaryFacilitator( - new DistracterFilterCheckingExactMatchesAndSuggestions(this /* context */)); + new DictionaryFacilitator(this /* context */); // TODO: Move from LatinIME. private final PersonalizationDictionaryUpdater mPersonalizationDictionaryUpdater = new PersonalizationDictionaryUpdater(this /* context */, mDictionaryFacilitator); @@ -162,6 +162,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private final SubtypeSwitcher mSubtypeSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); private final SpecialKeyDetector mSpecialKeyDetector; + private StatsUtilsManager mStatsUtilsManager; // Object for reacting to adding/removing a dictionary pack. private final BroadcastReceiver mDictionaryPackInstallReceiver = @@ -542,6 +543,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); mSpecialKeyDetector = new SpecialKeyDetector(this); + mStatsUtilsManager = StatsUtilsManager.getInstance(); mIsHardwareAcceleratedDrawingEnabled = InputMethodServiceCompatUtils.enableHardwareAcceleration(this); Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); @@ -557,8 +559,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen KeyboardSwitcher.init(this); AudioAndHapticFeedbackManager.init(this); AccessibilityUtils.init(this); - StatsUtils.init(this); - + mStatsUtilsManager.onCreate(this /* context */); super.onCreate(); mHandler.onCreate(); @@ -590,8 +591,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen registerReceiver(mDictionaryDumpBroadcastReceiver, dictDumpFilter); DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this); - - StatsUtils.onCreate(mSettings.getCurrent()); + StatsUtils.onCreate(mSettings.getCurrent(), mRichImm); } // Has to be package-visible for unit tests @@ -613,14 +613,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mDictionaryFacilitator.updateEnabledSubtypes(mRichImm.getMyEnabledInputMethodSubtypeList( true /* allowsImplicitlySelectedSubtypes */)); refreshPersonalizationDictionarySession(currentSettingsValues); - StatsUtils.onLoadSettings(currentSettingsValues); + mStatsUtilsManager.onLoadSettings(currentSettingsValues); } private void refreshPersonalizationDictionarySession( final SettingsValues currentSettingsValues) { - mPersonalizationDictionaryUpdater.onLoadSettings( - currentSettingsValues.mUsePersonalizedDicts, + mDictionaryFacilitator.setIsMonolingualUser( mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes()); + mPersonalizationDictionaryUpdater.onLoadSettings( + currentSettingsValues.mUsePersonalizedDicts); mContextualDictionaryUpdater.onLoadSettings(currentSettingsValues.mUsePersonalizedDicts); final boolean shouldKeepUserHistoryDictionaries; if (currentSettingsValues.mUsePersonalizedDicts) { @@ -703,7 +704,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen unregisterReceiver(mConnectivityAndRingerModeChangeReceiver); unregisterReceiver(mDictionaryPackInstallReceiver); unregisterReceiver(mDictionaryDumpBroadcastReceiver); - StatsUtils.onDestroy(); + mStatsUtilsManager.onDestroy(); super.onDestroy(); } @@ -738,15 +739,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen cleanupInternalStateForFinishInput(); } } - // TODO: Remove this test. - if (!conf.locale.equals(mPersonalizationDictionaryUpdater.getLocale())) { - refreshPersonalizationDictionarySession(settingsValues); - } super.onConfigurationChanged(conf); } @Override public View onCreateInputView() { + StatsUtils.onCreateInputView(); return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); } @@ -822,6 +820,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onFinishInputView(final boolean finishingInput) { + StatsUtils.onFinishInputView(); mHandler.onFinishInputView(finishingInput); } @@ -834,7 +833,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() // is not guaranteed. It may even be called at the same time on a different thread. - mSubtypeSwitcher.onSubtypeChanged(subtype); + final RichInputMethodSubtype richSubtype = new RichInputMethodSubtype(subtype); + mSubtypeSwitcher.onSubtypeChanged(richSubtype); mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype), mSettings.getCurrent()); loadKeyboard(); @@ -898,6 +898,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); final boolean isDifferentTextField = !restarting || inputTypeChanged; + + StatsUtils.onStartInputView(editorInfo.inputType, + Settings.getInstance().getCurrent().mDisplayOrientation, + !isDifferentTextField); + if (isDifferentTextField) { mSubtypeSwitcher.updateParametersOnStartInputView(); } @@ -1260,10 +1265,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mInputLogic.getCurrentRecapitalizeState(); } - public Locale getCurrentSubtypeLocale() { - return mSubtypeSwitcher.getCurrentSubtypeLocale(); - } - /** * @param codePoints code points to get coordinates for. * @return x,y coordinates for this keyboard, as a flattened array. @@ -1286,13 +1287,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Probably never supposed to happen, but just in case. return; } - final String wordToEdit; - if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { - wordToEdit = word.toLowerCase(getCurrentSubtypeLocale()); - } else { - wordToEdit = word; - } - mDictionaryFacilitator.addWordToUserDictionary(this /* context */, wordToEdit); + mDictionaryFacilitator.addWordToUserDictionary(this /* context */, word); mInputLogic.onAddWordToUserDictionary(); } @@ -1350,48 +1345,57 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeState.switchSubtype(token, mRichImm); } + // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for + // alphabetic shift and shift while in symbol layout and get rid of this method. + private int getCodePointForKeyboard(final int codePoint) { + if (Constants.CODE_SHIFT == codePoint) { + final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard(); + if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { + return codePoint; + } else { + return Constants.CODE_SYMBOL_SHIFT; + } + } else { + return codePoint; + } + } + // Implementation of {@link KeyboardActionListener}. @Override public void onCodeInput(final int codePoint, final int x, final int y, final boolean isKeyRepeat) { + // TODO: this processing does not belong inside LatinIME, the caller should be doing this. final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); // x and y include some padding, but everything down the line (especially native // code) needs the coordinates in the keyboard frame. // TODO: We should reconsider which coordinate system should be used to represent // keyboard event. Also we should pull this up -- LatinIME has no business doing - // this transformation, it should be done already before calling onCodeInput. + // this transformation, it should be done already before calling onEvent. final int keyX = mainKeyboardView.getKeyX(x); final int keyY = mainKeyboardView.getKeyY(y); - final int codeToSend; - if (Constants.CODE_SHIFT == codePoint) { - // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for - // alphabetic shift and shift while in symbol layout. - final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard(); - if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { - codeToSend = codePoint; - } else { - codeToSend = Constants.CODE_SYMBOL_SHIFT; - } - } else { - codeToSend = codePoint; - } - if (Constants.CODE_SHORTCUT == codePoint) { + final Event event = createSoftwareKeypressEvent(getCodePointForKeyboard(codePoint), + keyX, keyY, isKeyRepeat); + onEvent(event); + } + + // This method is public for testability of LatinIME, but also in the future it should + // completely replace #onCodeInput. + public void onEvent(final Event event) { + if (Constants.CODE_SHORTCUT == event.mKeyCode) { mSubtypeSwitcher.switchToShortcutIME(this); - // Still call the *#onCodeInput methods for readability. } - final Event event = createSoftwareKeypressEvent(codeToSend, keyX, keyY, isKeyRepeat); final InputTransaction completeInputTransaction = mInputLogic.onCodeInput(mSettings.getCurrent(), event, mKeyboardSwitcher.getKeyboardShiftMode(), mKeyboardSwitcher.getCurrentKeyboardScriptId(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); - mKeyboardSwitcher.onCodeInput(codePoint, getCurrentAutoCapsState(), - getCurrentRecapitalizeState()); + mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } // A helper method to split the code point and the key code. Ultimately, they should not be // squashed into the same variable, and this method should be removed. - private static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX, + // public for testing, as we don't want to copy the same logic into test code + public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX, final int keyY, final boolean isKeyRepeat) { final int keyCode; final int codePoint; @@ -1409,13 +1413,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onTextInput(final String rawText) { // TODO: have the keyboard pass the correct key code when we need it. - final Event event = Event.createSoftwareTextEvent(rawText, Event.NOT_A_KEY_CODE); + final Event event = Event.createSoftwareTextEvent(rawText, Constants.CODE_OUTPUT_TEXT); final InputTransaction completeInputTransaction = mInputLogic.onTextInput(mSettings.getCurrent(), event, mKeyboardSwitcher.getKeyboardShiftMode(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); - mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT, getCurrentAutoCapsState(), - getCurrentRecapitalizeState()); + mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } @Override @@ -1573,7 +1576,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (!hasSuggestionStripView()) { return; } - mSuggestionStripView.showAddToDictionaryHint(word); + final String wordToShow; + if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { + wordToShow = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); + } else { + wordToShow = word; + } + mSuggestionStripView.showAddToDictionaryHint(wordToShow); } // This will show either an empty suggestion strip (if prediction is enabled) or diff --git a/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java new file mode 100644 index 000000000..43cebdfa4 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; + +import android.content.Context; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback; +import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; +import com.android.inputmethod.latin.personalization.PersonalizationDictionary; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.utils.DistracterFilter; +import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; +import com.android.inputmethod.latin.utils.LanguageModelParam; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; + +/** + * Class for managing and updating personalization dictionaries. + */ +public class PersonalizationHelperForDictionaryFacilitator { + private final Context mContext; + private final DistracterFilter mDistracterFilter; + private final HashMap<String, HashSet<Locale>> mLangToLocalesMap = new HashMap<>(); + private final HashMap<Locale, ExpandableBinaryDictionary> mPersonalizationDictsToUpdate = + new HashMap<>(); + private boolean mIsMonolingualUser = false; + + PersonalizationHelperForDictionaryFacilitator(final Context context, + final DistracterFilter distracterFilter) { + mContext = context; + mDistracterFilter = distracterFilter; + } + + public void close() { + mLangToLocalesMap.clear(); + for (final ExpandableBinaryDictionary dict : mPersonalizationDictsToUpdate.values()) { + dict.close(); + } + mPersonalizationDictsToUpdate.clear(); + } + + public void clearDictionariesToUpdate() { + for (final ExpandableBinaryDictionary dict : mPersonalizationDictsToUpdate.values()) { + dict.clear(); + } + mPersonalizationDictsToUpdate.clear(); + } + + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { + for (final InputMethodSubtype subtype : enabledSubtypes) { + final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype); + final String language = locale.getLanguage(); + final HashSet<Locale> locales = mLangToLocalesMap.get(language); + if (locales != null) { + locales.add(locale); + } else { + final HashSet<Locale> localeSet = new HashSet<>(); + localeSet.add(locale); + mLangToLocalesMap.put(language, localeSet); + } + } + } + + public void setIsMonolingualUser(final boolean isMonolingualUser) { + mIsMonolingualUser = isMonolingualUser; + } + + /** + * Flush personalization dictionaries to dictionary files. Close dictionaries after writing + * files except the dictionary that is used for generating suggestions. + * + * @param personalizationDictUsedForSuggestion the personalization dictionary used for + * generating suggestions that won't be closed. + */ + public void flushPersonalizationDictionariesToUpdate( + final ExpandableBinaryDictionary personalizationDictUsedForSuggestion) { + for (final ExpandableBinaryDictionary personalizationDict : + mPersonalizationDictsToUpdate.values()) { + personalizationDict.asyncFlushBinaryDictionary(); + if (personalizationDict != personalizationDictUsedForSuggestion) { + // Close if the dictionary is not being used for suggestion. + personalizationDict.close(); + } + } + mDistracterFilter.close(); + mPersonalizationDictsToUpdate.clear(); + } + + private ExpandableBinaryDictionary getPersonalizationDictToUpdate(final Context context, + final Locale locale) { + ExpandableBinaryDictionary personalizationDict = mPersonalizationDictsToUpdate.get(locale); + if (personalizationDict != null) { + return personalizationDict; + } + personalizationDict = PersonalizationDictionary.getDictionary(context, locale, + null /* dictFile */, "" /* dictNamePrefix */); + mPersonalizationDictsToUpdate.put(locale, personalizationDict); + return personalizationDict; + } + + private void addEntriesToPersonalizationDictionariesForLocale(final Locale locale, + final PersonalizationDataChunk personalizationDataChunk, + final SpacingAndPunctuations spacingAndPunctuations, + final AddMultipleDictionaryEntriesCallback callback) { + final ExpandableBinaryDictionary personalizationDict = + getPersonalizationDictToUpdate(mContext, locale); + if (personalizationDict == null) { + if (callback != null) { + callback.onFinished(); + } + return; + } + final ArrayList<LanguageModelParam> languageModelParams = + LanguageModelParam.createLanguageModelParamsFrom( + personalizationDataChunk.mTokens, + personalizationDataChunk.mTimestampInSeconds, spacingAndPunctuations, + locale, new DistracterFilterCheckingIsInDictionary( + mDistracterFilter, personalizationDict)); + if (languageModelParams == null || languageModelParams.isEmpty()) { + if (callback != null) { + callback.onFinished(); + } + return; + } + personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback); + } + + public void addEntriesToPersonalizationDictionariesToUpdate(final Locale defaultLocale, + final PersonalizationDataChunk personalizationDataChunk, + final SpacingAndPunctuations spacingAndPunctuations, + final AddMultipleDictionaryEntriesCallback callback) { + final String language = personalizationDataChunk.mDetectedLanguage; + final HashSet<Locale> locales; + if (mIsMonolingualUser && PersonalizationDataChunk.LANGUAGE_UNKNOWN.equals(language) + && mLangToLocalesMap.size() == 1) { + locales = mLangToLocalesMap.get(defaultLocale.getLanguage()); + } else { + locales = mLangToLocalesMap.get(language); + } + if (locales == null || locales.isEmpty()) { + if (callback != null) { + callback.onFinished(); + } + return; + } + final AtomicInteger remainingTaskCount = new AtomicInteger(locales.size()); + final AddMultipleDictionaryEntriesCallback callbackForLocales = + new AddMultipleDictionaryEntriesCallback() { + @Override + public void onFinished() { + if (remainingTaskCount.decrementAndGet() == 0) { + // Update tasks for all locales have been finished. + if (callback != null) { + callback.onFinished(); + } + } + } + }; + for (final Locale locale : locales) { + addEntriesToPersonalizationDictionariesForLocale(locale, personalizationDataChunk, + spacingAndPunctuations, callbackForLocales); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java index 5d4fc5861..ecf25c28b 100644 --- a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java @@ -40,7 +40,7 @@ public final class ReadOnlyBinaryDictionary extends Dictionary { public ReadOnlyBinaryDictionary(final String filename, final long offset, final long length, final boolean useFullEditDistance, final Locale locale, final String dictType) { - super(dictType); + super(dictType, locale); mBinaryDictionary = new BinaryDictionary(filename, offset, length, useFullEditDistance, locale, dictType, false /* isUpdatable */); } diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index b5d42dd04..dc00ecc8f 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -737,17 +737,19 @@ public final class RichInputConnection { return TextUtils.equals(text, beforeText); } - public boolean revertDoubleSpacePeriod() { + public boolean revertDoubleSpacePeriod(final SpacingAndPunctuations spacingAndPunctuations) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); // Here we test whether we indeed have a period and a space before us. This should not // be needed, but it's there just in case something went wrong. final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); - if (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, textBeforeCursor)) { + if (!TextUtils.equals(spacingAndPunctuations.mSentenceSeparatorAndSpace, + textBeforeCursor)) { // Theoretically we should not be coming here if there isn't ". " before the // cursor, but the application may be changing the text while we are typing, so // anything goes. We should not crash. - Log.d(TAG, "Tried to revert double-space combo but we didn't find " - + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor."); + Log.d(TAG, "Tried to revert double-space combo but we didn't find \"" + + spacingAndPunctuations.mSentenceSeparatorAndSpace + + "\" just before the cursor."); return false; } // Double-space results in ". ". A backspace to cancel this should result in a single diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java index 7cf4eff92..0d5ce7d6d 100644 --- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java @@ -40,7 +40,8 @@ import java.util.List; /** * Enrichment class for InputMethodManager to simplify interaction and add functionality. */ -public final class RichInputMethodManager { +// non final for easy mocking. +public class RichInputMethodManager { private static final String TAG = RichInputMethodManager.class.getSimpleName(); private RichInputMethodManager() { @@ -297,10 +298,14 @@ public final class RichInputMethodManager { return INDEX_NOT_FOUND; } - public InputMethodSubtype getCurrentInputMethodSubtype( - final InputMethodSubtype defaultSubtype) { + public RichInputMethodSubtype getCurrentInputMethodSubtype( + final RichInputMethodSubtype defaultSubtype) { final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype(); - return (currentSubtype != null) ? currentSubtype : defaultSubtype; + if (currentSubtype == null) { + return defaultSubtype; + } + // TODO: Determine locales to use for multi-lingual use. + return new RichInputMethodSubtype(currentSubtype); } public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) { diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java new file mode 100644 index 000000000..0b08c48e5 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.utils.LocaleUtils; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; + +import java.util.Arrays; +import java.util.Locale; + +/** + * Enrichment class for InputMethodSubtype to enable concurrent multi-lingual input. + * + * Right now, this returns the extra value of its primary subtype. + */ +public final class RichInputMethodSubtype { + private final InputMethodSubtype mSubtype; + private final Locale[] mLocales; + + public RichInputMethodSubtype(final InputMethodSubtype subtype, final Locale... locales) { + mSubtype = subtype; + mLocales = new Locale[locales.length+1]; + mLocales[0] = LocaleUtils.constructLocaleFromString(mSubtype.getLocale()); + System.arraycopy(locales, 0, mLocales, 1, locales.length); + } + + // Extra values are determined by the primary subtype. This is probably right, but + // we may have to revisit this later. + public String getExtraValueOf(final String key) { + return mSubtype.getExtraValueOf(key); + } + + // The mode is also determined by the primary subtype. + public String getMode() { + return mSubtype.getMode(); + } + + public boolean isNoLanguage() { + if (mLocales.length > 1) { + return false; + } + return SubtypeLocaleUtils.NO_LANGUAGE.equals(mSubtype.getLocale()); + } + + public String getNameForLogging() { + return toString(); + } + + // InputMethodSubtype's display name for spacebar text in its locale. + // isAdditionalSubtype (T=true, F=false) + // locale layout | Middle Full + // ------ ------- - --------- ---------------------- + // en_US qwerty F English English (US) exception + // en_GB qwerty F English English (UK) exception + // es_US spanish F Español Español (EE.UU.) exception + // fr azerty F Français Français + // fr_CA qwerty F Français Français (Canada) + // fr_CH swiss F Français Français (Suisse) + // de qwertz F Deutsch Deutsch + // de_CH swiss T Deutsch Deutsch (Schweiz) + // zz qwerty F QWERTY QWERTY + // fr qwertz T Français Français + // de qwerty T Deutsch Deutsch + // en_US azerty T English English (US) + // zz azerty T AZERTY AZERTY + // Get the RichInputMethodSubtype's full display name in its locale. + public String getFullDisplayName() { + if (isNoLanguage()) { + return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype); + } + return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(mSubtype.getLocale()); + } + + // Get the RichInputMethodSubtype's middle display name in its locale. + public String getMiddleDisplayName() { + if (isNoLanguage()) { + return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype); + } + return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(mSubtype.getLocale()); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RichInputMethodSubtype)) { + return false; + } + final RichInputMethodSubtype other = (RichInputMethodSubtype)o; + return mSubtype.equals(other.mSubtype) && Arrays.equals(mLocales, other.mLocales); + } + + @Override + public int hashCode() { + return mSubtype.hashCode() + Arrays.hashCode(mLocales); + } + + @Override + public String toString() { + return "Multi-lingual subtype: " + mSubtype.toString() + ", " + Arrays.toString(mLocales); + } + + // TODO: remove this method! We can always have several locales. Multi-lingual input will only + // be done when this method is gone. + public String getLocale() { + return mSubtype.getLocale(); + } + + // TODO: remove this method + public InputMethodSubtype getRawSubtype() { return mSubtype; } +} diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index a725e1611..45d67ff88 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -58,8 +58,8 @@ public final class SubtypeSwitcher { new LanguageOnSpacebarHelper(); private InputMethodInfo mShortcutInputMethodInfo; private InputMethodSubtype mShortcutSubtype; - private InputMethodSubtype mNoLanguageSubtype; - private InputMethodSubtype mEmojiSubtype; + private RichInputMethodSubtype mNoLanguageSubtype; + private RichInputMethodSubtype mEmojiSubtype; private boolean mIsNetworkConnected; private static final String KEYBOARD_MODE = "keyboard"; @@ -70,26 +70,26 @@ public final class SubtypeSwitcher { + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; - private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = - InputMethodSubtypeCompatUtils.newInputMethodSubtype( + private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = + new RichInputMethodSubtype(InputMethodSubtypeCompatUtils.newInputMethodSubtype( R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark, SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE, EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE, false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */, - SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE); + SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE)); // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}. // Dummy Emoji subtype. See {@link R.xml.method}. private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0; private static final String EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE = "KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; - private static final InputMethodSubtype DUMMY_EMOJI_SUBTYPE = + private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE = new RichInputMethodSubtype( InputMethodSubtypeCompatUtils.newInputMethodSubtype( R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark, SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE, EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE, false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */, - SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE); + SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE)); public static SubtypeSwitcher getInstance() { return sInstance; @@ -165,18 +165,17 @@ public final class SubtypeSwitcher { } // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. - public void onSubtypeChanged(final InputMethodSubtype newSubtype) { + public void onSubtypeChanged(final RichInputMethodSubtype newSubtype) { if (DBG) { - Log.w(TAG, "onSubtypeChanged: " - + SubtypeLocaleUtils.getSubtypeNameForLogging(newSubtype)); + Log.w(TAG, "onSubtypeChanged: " + newSubtype.getNameForLogging()); } final Locale newLocale = SubtypeLocaleUtils.getSubtypeLocale(newSubtype); final Locale systemLocale = mResources.getConfiguration().locale; final boolean sameLocale = systemLocale.equals(newLocale); final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage()); - final boolean implicitlyEnabled = - mRichImm.checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype); + final boolean implicitlyEnabled = mRichImm + .checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype.getRawSubtype()); mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage( sameLocale || (sameLanguage && implicitlyEnabled)); @@ -250,7 +249,7 @@ public final class SubtypeSwitcher { // Subtype Switching functions // ////////////////////////////////// - public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { + public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) { return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype); } @@ -279,10 +278,10 @@ public final class SubtypeSwitcher { return true; } - private static InputMethodSubtype sForcedSubtypeForTesting = null; + private static RichInputMethodSubtype sForcedSubtypeForTesting = null; @UsedForTesting void forceSubtype(final InputMethodSubtype subtype) { - sForcedSubtypeForTesting = subtype; + sForcedSubtypeForTesting = new RichInputMethodSubtype(subtype); } public Locale getCurrentSubtypeLocale() { @@ -292,17 +291,18 @@ public final class SubtypeSwitcher { return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype()); } - public InputMethodSubtype getCurrentSubtype() { + public RichInputMethodSubtype getCurrentSubtype() { if (null != sForcedSubtypeForTesting) { return sForcedSubtypeForTesting; } return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype()); } - public InputMethodSubtype getNoLanguageSubtype() { + public RichInputMethodSubtype getNoLanguageSubtype() { if (mNoLanguageSubtype == null) { - mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( - SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY); + mNoLanguageSubtype = new RichInputMethodSubtype( + mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY)); } if (mNoLanguageSubtype != null) { return mNoLanguageSubtype; @@ -313,10 +313,14 @@ public final class SubtypeSwitcher { return DUMMY_NO_LANGUAGE_SUBTYPE; } - public InputMethodSubtype getEmojiSubtype() { + public RichInputMethodSubtype getEmojiSubtype() { if (mEmojiSubtype == null) { - mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( - SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI); + final InputMethodSubtype rawEmojiSubtype = + mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI); + if (null != rawEmojiSubtype) { + mEmojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype); + } } if (mEmojiSubtype != null) { return mEmojiSubtype; @@ -328,6 +332,6 @@ public final class SubtypeSwitcher { } public String getCombiningRulesExtraValueOfCurrentSubtype() { - return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype()); + return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype()); } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 6779351fd..9e4aa40a2 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -84,7 +84,7 @@ public final class Suggest { private static ArrayList<SuggestedWordInfo> getTransformedSuggestedWordInfoList( final WordComposer wordComposer, final SuggestionResults results, - final int trailingSingleQuotesCount) { + final int trailingSingleQuotesCount, final Locale defaultLocale) { final boolean shouldMakeSuggestionsAllUpperCase = wordComposer.isAllUpperCase() && !wordComposer.isResumed(); final boolean isOnlyFirstCharCapitalized = @@ -96,9 +96,11 @@ public final class Suggest { || 0 != trailingSingleQuotesCount) { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); + final Locale wordLocale = wordInfo.mSourceDict.mLocale; final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( - wordInfo, results.mLocale, shouldMakeSuggestionsAllUpperCase, - isOnlyFirstCharCapitalized, trailingSingleQuotesCount); + wordInfo, null == wordLocale ? defaultLocale : wordLocale, + shouldMakeSuggestionsAllUpperCase, isOnlyFirstCharCapitalized, + trailingSingleQuotesCount); suggestionsContainer.set(i, transformedWordInfo); } } @@ -134,7 +136,7 @@ public final class Suggest { SESSION_ID_TYPING); final ArrayList<SuggestedWordInfo> suggestionsContainer = getTransformedSuggestedWordInfoList(wordComposer, suggestionResults, - trailingSingleQuotesCount); + trailingSingleQuotesCount, mDictionaryFacilitator.getLocale()); final boolean didRemoveTypedWord = SuggestedWordInfo.removeDups(wordComposer.getTypedWord(), suggestionsContainer); @@ -208,6 +210,7 @@ public final class Suggest { final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion, SESSION_ID_GESTURE); + final Locale defaultLocale = mDictionaryFacilitator.getLocale(); final ArrayList<SuggestedWordInfo> suggestionsContainer = new ArrayList<>(suggestionResults); final int suggestionsCount = suggestionsContainer.size(); @@ -216,9 +219,10 @@ public final class Suggest { if (isFirstCharCapitalized || isAllUpperCase) { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); + final Locale wordlocale = wordInfo.mSourceDict.mLocale; final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( - wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized, - 0 /* trailingSingleQuotesCount */); + wordInfo, null == wordlocale ? defaultLocale : wordlocale, isAllUpperCase, + isFirstCharCapitalized, 0 /* trailingSingleQuotesCount */); suggestionsContainer.set(i, transformedWordInfo); } } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 6bc3da885..dcfaa3f6d 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -142,6 +142,15 @@ public class SuggestedWords { return mSuggestedWordInfoList.get(index); } + /** + * Gets the suggestion index from the suggestions list. + * @param suggestedWordInfo The {@link SuggestedWordInfo} to find the index. + * @return The position of the suggestion in the suggestion list. + */ + public int indexOf(SuggestedWordInfo suggestedWordInfo) { + return mSuggestedWordInfoList.indexOf(suggestedWordInfo); + } + public String getDebugString(final int pos) { if (!DebugFlags.DEBUG_ENABLED) { return null; diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 5ab3571e6..c5e60d677 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -60,6 +60,7 @@ import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.AsyncResultHolder; import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.RecapitalizeStatus; +import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.TextRange; @@ -361,6 +362,8 @@ public final class InputLogic { if (shouldShowAddToDictionaryIndicator) { mTextDecorator.showAddToDictionaryIndicator(suggestionInfo); } + + StatsUtils.onPickSuggestionManually(mSuggestedWords, suggestionInfo); return inputTransaction; } @@ -726,7 +729,7 @@ public final class InputLogic { break; case Constants.CODE_CAPSLOCK: // Note: Changing keyboard to shift lock state is handled in - // {@link KeyboardSwitcher#onCodeInput(int)}. + // {@link KeyboardSwitcher#onEvent(Event)}. break; case Constants.CODE_SYMBOL_SHIFT: // Note: Calling back to the keyboard on the symbol Shift key is handled in @@ -754,11 +757,11 @@ public final class InputLogic { break; case Constants.CODE_EMOJI: // Note: Switching emoji keyboard is being handled in - // {@link KeyboardState#onCodeInput(int,int)}. + // {@link KeyboardState#onEvent(Event,int)}. break; case Constants.CODE_ALPHA_FROM_EMOJI: // Note: Switching back from Emoji keyboard to the main keyboard is being - // handled in {@link KeyboardState#onCodeInput(int,int)}. + // handled in {@link KeyboardState#onEvent(Event,int)}. break; case Constants.CODE_SHIFT_ENTER: // TODO: remove this object @@ -1085,8 +1088,10 @@ public final class InputLogic { if (!TextUtils.isEmpty(rejectedSuggestion)) { mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion); } + StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length()); } else { mWordComposer.applyProcessedEvent(event); + StatsUtils.onBackspacePressed(1); } if (mWordComposer.isComposingWord()) { setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); @@ -1097,6 +1102,7 @@ public final class InputLogic { } else { if (mLastComposedWord.canRevertCommit()) { revertCommit(inputTransaction); + StatsUtils.onRevertAutoCorrect(); return; } if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { @@ -1104,6 +1110,7 @@ public final class InputLogic { // This is triggered on backspace after a key that inputs multiple characters, // like the smiley key or the .com key. mConnection.deleteSurroundingText(mEnteredText.length(), 0); + StatsUtils.onDeleteMultiCharInput(mEnteredText.length()); mEnteredText = null; // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. // In addition we know that spaceState is false, and that we should not be @@ -1112,16 +1119,19 @@ public final class InputLogic { } if (SpaceState.DOUBLE == inputTransaction.mSpaceState) { cancelDoubleSpacePeriodCountdown(); - if (mConnection.revertDoubleSpacePeriod()) { + if (mConnection.revertDoubleSpacePeriod( + inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { // No need to reset mSpaceState, it has already be done (that's why we // receive it as a parameter) inputTransaction.setRequiresUpdateSuggestions(); mWordComposer.setCapitalizedModeAtStartComposingTime( WordComposer.CAPS_MODE_OFF); + StatsUtils.onRevertDoubleSpacePeriod(); return; } } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { if (mConnection.revertSwapPunctuation()) { + StatsUtils.onRevertSwapPunctuation(); // Likewise return; } @@ -1136,6 +1146,7 @@ public final class InputLogic { mConnection.setSelection(mConnection.getExpectedSelectionEnd(), mConnection.getExpectedSelectionEnd()); mConnection.deleteSurroundingText(numCharsDeleted, 0); + StatsUtils.onBackspaceSelectedText(numCharsDeleted); } else { // There is no selection, just delete one character. if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) { @@ -1152,9 +1163,12 @@ public final class InputLogic { // applications are relying on this behavior so we continue to support it for // older apps, so we retain this behavior if the app has target SDK < JellyBean. sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + int totalDeletedLength = 1; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + totalDeletedLength++; } + StatsUtils.onBackspacePressed(totalDeletedLength); } else { final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); if (codePointBeforeCursor == Constants.NOT_A_CODE) { @@ -1165,11 +1179,13 @@ public final class InputLogic { // catch it and have their broken interface react. If you need the keyboard // to do this, you're doing it wrong -- please fix your app. mConnection.deleteSurroundingText(1, 0); + // TODO: Add a new StatsUtils method onBackspaceWhenNoText() return; } final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; mConnection.deleteSurroundingText(lengthToDelete, 0); + int totalDeletedLength = lengthToDelete; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { final int codePointBeforeCursorToDeleteAgain = mConnection.getCodePointBeforeCursor(); @@ -1177,8 +1193,10 @@ public final class InputLogic { final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( codePointBeforeCursorToDeleteAgain) ? 2 : 1; mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + totalDeletedLength += lengthToDeleteAgain; } } + StatsUtils.onBackspacePressed(totalDeletedLength); } } if (inputTransaction.mSettingsValues @@ -1295,7 +1313,9 @@ public final class InputLogic { if (null == lastTwo) return false; final int length = lastTwo.length(); if (length < 2) return false; - if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) return false; + if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) { + return false; + } // We know there is a space in pos -1, and we have at least two chars. If we have only two // chars, isSurrogatePairs can't return true as charAt(1) is a space, so this is fine. final int firstCodePoint = diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index a2ae74b20..ec3c6e291 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -36,9 +36,7 @@ public final class FormatSpec { * sion * * o | - * p | not used 3 bits - * t | each unigram and bigram entry has a time stamp? - * i | 1 bit, 1 = yes, 0 = no : CONTAINS_TIMESTAMP_FLAG + * p | not used, 2 bytes. * o | * nflags * @@ -48,7 +46,7 @@ public final class FormatSpec { * d | * ersize * - * | attributes list + * attributes list * * attributes list is: * <key> = | string of characters at the char format described below, with the terminator used @@ -86,11 +84,10 @@ public final class FormatSpec { */ /* Node (FusionDictionary.PtNode) layout is as follows: - * | is moved ? 2 bits, 11 = no : FLAG_IS_NOT_MOVED - * | This must be the same as FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES - * | 01 = yes : FLAG_IS_MOVED - * f | the new address is stored in the same place as the parent address - * l | is deleted? 10 = yes : FLAG_IS_DELETED + * | CHILDREN_ADDRESS_TYPE 2 bits, 11 : FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES + * | 10 : FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES + * f | 01 : FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE + * l | 00 : FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS * a | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS * g | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL * s | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS @@ -98,16 +95,6 @@ public final class FormatSpec { * | is not a word ? 1 bit, 1 = yes, 0 = no : FLAG_IS_NOT_A_WORD * | is blacklisted ? 1 bit, 1 = yes, 0 = no : FLAG_IS_BLACKLISTED * - * p | - * a | parent address, 3byte - * r | 1 byte = bbbbbbbb match - * e | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) - * n | otherwise => (bbbbbbbb << 16) + (next byte << 8) + next byte - * t | This address is relative to the head of the PtNode. - * a | If the node doesn't have a parent, this field is set to 0. - * d | - * dress - * * c | IF FLAG_HAS_MULTIPLE_CHARS * h | char, char, char, char n * (1 or 3 bytes) : use PtNodeInfo for i/o helpers * a | end 1 byte, = 0 @@ -121,15 +108,10 @@ public final class FormatSpec { * q | * * c | - * h | children address, 3 bytes - * i | 1 byte = bbbbbbbb match - * l | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) - * d | otherwise => (bbbbbbbb<<16) + (next byte << 8) + next byte - * r | if this node doesn't have children, this field is set to 0. - * e | (see BinaryDictEncoderUtils#writeVariableSignedAddress) - * n | This address is relative to the position of this field. - * a | - * ddress + * h | children address, CHILDREN_ADDRESS_TYPE bytes + * i | This address is relative to the position of this field. + * l | + * drenaddress * * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS * | shortcut string list @@ -179,8 +161,6 @@ public final class FormatSpec { public static final int MAGIC_NUMBER = 0x9BC13AFE; static final int NOT_A_VERSION_NUMBER = -1; - static final int FIRST_VERSION_WITH_DYNAMIC_UPDATE = 3; - static final int FIRST_VERSION_WITH_TERMINAL_ID = 4; // These MUST have the same values as the relevant constants in format_utils.h. // From version 4 on, we use version * 100 + revision as a version number. That allows @@ -202,9 +182,6 @@ public final class FormatSpec { // use it in the reading code. static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; - static final int PARENT_ADDRESS_SIZE = 3; - static final int FORWARD_LINK_ADDRESS_SIZE = 3; - // These flags are used only in the static dictionary. static final int MASK_CHILDREN_ADDRESS_TYPE = 0xC0; static final int FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS = 0x00; @@ -220,13 +197,6 @@ public final class FormatSpec { static final int FLAG_IS_NOT_A_WORD = 0x02; static final int FLAG_IS_BLACKLISTED = 0x01; - // These flags are used only in the dynamic dictionary. - static final int MASK_MOVE_AND_DELETE_FLAG = 0xC0; - static final int FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE = 0x40; - static final int FLAG_IS_MOVED = 0x00 | FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE; - static final int FLAG_IS_NOT_MOVED = 0x80 | FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE; - static final int FLAG_IS_DELETED = 0x80; - static final int FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT = 0x80; static final int FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE = 0x40; static final int MASK_BIGRAM_ATTR_ADDRESS_TYPE = 0x30; @@ -240,52 +210,12 @@ public final class FormatSpec { static final int PTNODE_TERMINATOR_SIZE = 1; static final int PTNODE_FLAGS_SIZE = 1; static final int PTNODE_FREQUENCY_SIZE = 1; - static final int PTNODE_TERMINAL_ID_SIZE = 4; static final int PTNODE_MAX_ADDRESS_SIZE = 3; static final int PTNODE_ATTRIBUTE_FLAGS_SIZE = 1; static final int PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE = 3; static final int PTNODE_SHORTCUT_LIST_SIZE_SIZE = 2; - // These values are used only by version 4 or later. They MUST match the definitions in - // ver4_dict_constants.cpp. - static final String TRIE_FILE_EXTENSION = ".trie"; - public static final String HEADER_FILE_EXTENSION = ".header"; - static final String FREQ_FILE_EXTENSION = ".freq"; - // tat = Terminal Address Table - static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat"; - static final String BIGRAM_FILE_EXTENSION = ".bigram"; - static final String SHORTCUT_FILE_EXTENSION = ".shortcut"; - static final String LOOKUP_TABLE_FILE_SUFFIX = "_lookup"; - static final String CONTENT_TABLE_FILE_SUFFIX = "_index"; - static final int FLAGS_IN_FREQ_FILE_SIZE = 1; - static final int FREQUENCY_AND_FLAGS_SIZE = 2; - static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3; - static final int UNIGRAM_TIMESTAMP_SIZE = 4; - static final int UNIGRAM_COUNTER_SIZE = 1; - static final int UNIGRAM_LEVEL_SIZE = 1; - - // With the English main dictionary as of October 2013, the size of bigram address table is - // is 345KB with the block size being 16. - // This is 54% of that of full address table. - static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 16; - static final int BIGRAM_CONTENT_COUNT = 1; - static final int BIGRAM_FREQ_CONTENT_INDEX = 0; - static final String BIGRAM_FREQ_CONTENT_ID = "_freq"; - static final int BIGRAM_TIMESTAMP_SIZE = 4; - static final int BIGRAM_COUNTER_SIZE = 1; - static final int BIGRAM_LEVEL_SIZE = 1; - - static final int SHORTCUT_CONTENT_COUNT = 1; - static final int SHORTCUT_CONTENT_INDEX = 0; - // With the English main dictionary as of October 2013, the size of shortcut address table is - // 26KB with the block size being 64. - // This is only 4.4% of that of full address table. - static final int SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE = 64; - static final String SHORTCUT_CONTENT_ID = "_shortcut"; - static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE; - static final int NO_PARENT_ADDRESS = 0; - static final int NO_FORWARD_LINK_ADDRESS = 0; static final int INVALID_CHARACTER = -1; static final int MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT = 0x7F; // 127 @@ -302,14 +232,11 @@ public final class FormatSpec { // This option needs to be the same numeric value as the one in binary_format.h. static final int NOT_VALID_WORD = -99; - static final int SIGNED_CHILDREN_ADDRESS_SIZE = 3; static final int UINT8_MAX = 0xFF; static final int UINT16_MAX = 0xFFFF; static final int UINT24_MAX = 0xFFFFFF; - static final int SINT24_MAX = 0x7FFFFF; static final int MSB8 = 0x80; - static final int MSB24 = 0x800000; /** * Options about file format. diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java index 9d72de8c5..734ed5583 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java @@ -18,20 +18,22 @@ package com.android.inputmethod.latin.personalization; import java.util.Collections; import java.util.List; -import java.util.Locale; public class PersonalizationDataChunk { + public static final String LANGUAGE_UNKNOWN = ""; + public final boolean mInputByUser; public final List<String> mTokens; public final int mTimestampInSeconds; public final String mPackageName; - public final Locale mlocale = null; + public final String mDetectedLanguage; public PersonalizationDataChunk(boolean inputByUser, final List<String> tokens, - final int timestampInSeconds, final String packageName) { + final int timestampInSeconds, final String packageName, final String detectedLanguage) { mInputByUser = inputByUser; mTokens = Collections.unmodifiableList(tokens); mTimestampInSeconds = timestampInSeconds; mPackageName = packageName; + mDetectedLanguage = detectedLanguage; } } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index 5c742a8b1..270b22a4a 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -40,7 +40,8 @@ import java.util.Locale; * When you call the constructor of this class, you may want to change the current system locale by * using {@link com.android.inputmethod.latin.utils.RunInLocale}. */ -public final class SettingsValues { +// Non-final for testing via mock library. +public class SettingsValues { private static final String TAG = SettingsValues.class.getSimpleName(); // "floatMaxValue" and "floatNegativeInfinity" are special marker strings for // Float.NEGATIVE_INFINITE and Float.MAX_VALUE. Currently used for auto-correction settings. @@ -167,8 +168,8 @@ public final class SettingsValues { autoCorrectionThresholdRawValue); mGestureInputEnabled = Settings.readGestureInputEnabled(prefs, res); mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true); - mGestureFloatingPreviewTextEnabled = prefs.getBoolean( - Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); + mGestureFloatingPreviewTextEnabled = !mInputAttributes.mDisableGestureFloatingPreviewText + && prefs.getBoolean(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); mPhraseGestureEnabled = Settings.readPhraseGestureEnabled(prefs, res); mAutoCorrectionEnabledPerUserSettings = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; @@ -220,6 +221,10 @@ public final class SettingsValues { } } + public boolean isMetricsLoggingEnabled() { + return mEnableMetricsLogging; + } + public boolean isApplicationSpecifiedCompletionsOn() { return mInputAttributes.mApplicationSpecifiedCompletionOn; } diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java index 49d81104d..97aad3b6d 100644 --- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java +++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java @@ -36,6 +36,8 @@ public final class SpacingAndPunctuations { public final int[] mSortedWordSeparators; public final PunctuationSuggestions mSuggestPuncList; private final int mSentenceSeparator; + private final int mAbbreviationMarker; + private final int[] mSortedSentenceTerminators; public final String mSentenceSeparatorAndSpace; public final boolean mCurrentLanguageHasSpaces; public final boolean mUsesAmericanTypography; @@ -55,7 +57,10 @@ public final class SpacingAndPunctuations { res.getString(R.string.symbols_word_connectors)); mSortedWordSeparators = StringUtils.toSortedCodePointArray( res.getString(R.string.symbols_word_separators)); + mSortedSentenceTerminators = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_sentence_terminators)); mSentenceSeparator = res.getInteger(R.integer.sentence_separator); + mAbbreviationMarker = res.getInteger(R.integer.abbreviation_marker); mSentenceSeparatorAndSpace = new String(new int[] { mSentenceSeparator, Constants.CODE_SPACE }, 0, 2); mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces); @@ -77,8 +82,10 @@ public final class SpacingAndPunctuations { mSortedSymbolsClusteringTogether = model.mSortedSymbolsClusteringTogether; mSortedWordConnectors = model.mSortedWordConnectors; mSortedWordSeparators = overrideSortedWordSeparators; + mSortedSentenceTerminators = model.mSortedSentenceTerminators; mSuggestPuncList = model.mSuggestPuncList; mSentenceSeparator = model.mSentenceSeparator; + mAbbreviationMarker = model.mAbbreviationMarker; mSentenceSeparatorAndSpace = model.mSentenceSeparatorAndSpace; mCurrentLanguageHasSpaces = model.mCurrentLanguageHasSpaces; mUsesAmericanTypography = model.mUsesAmericanTypography; @@ -109,6 +116,14 @@ public final class SpacingAndPunctuations { return Arrays.binarySearch(mSortedSymbolsClusteringTogether, code) >= 0; } + public boolean isSentenceTerminator(final int code) { + return Arrays.binarySearch(mSortedSentenceTerminators, code) >= 0; + } + + public boolean isAbbreviationMarker(final int code) { + return code == mAbbreviationMarker; + } + public boolean isSentenceSeparator(final int code) { return code == mSentenceSeparator; } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 90398deb2..49b34d391 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -16,14 +16,11 @@ package com.android.inputmethod.latin.spellcheck; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.service.textservice.SpellCheckerService; import android.text.InputType; -import android.util.Log; -import android.util.LruCache; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SuggestionsInfo; @@ -32,39 +29,21 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.ContactsBinaryDictionary; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.DictionaryCollection; import com.android.inputmethod.latin.DictionaryFacilitator; -import com.android.inputmethod.latin.DictionaryFactory; +import com.android.inputmethod.latin.DictionaryFacilitatorLruCache; import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; -import com.android.inputmethod.latin.UserBinaryDictionary; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; -import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.ScriptUtils; -import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.SuggestionResults; import com.android.inputmethod.latin.WordComposer; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; /** * Service for spell checking, using LatinIME's dictionaries and mechanisms. @@ -80,61 +59,28 @@ public final class AndroidSpellCheckerService extends SpellCheckerService private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368; private static final String DICTIONARY_NAME_PREFIX = "spellcheck_"; - private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000; - private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5; private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private final HashSet<Locale> mCachedLocales = new HashSet<>(); - private final int MAX_NUM_OF_THREADS_READ_DICTIONARY = 2; private final Semaphore mSemaphore = new Semaphore(MAX_NUM_OF_THREADS_READ_DICTIONARY, true /* fair */); // TODO: Make each spell checker session has its own session id. private final ConcurrentLinkedQueue<Integer> mSessionIdPool = new ConcurrentLinkedQueue<>(); - private static class DictionaryFacilitatorLruCache extends - LruCache<Locale, DictionaryFacilitator> { - private final HashSet<Locale> mCachedLocales; - public DictionaryFacilitatorLruCache(final HashSet<Locale> cachedLocales, int maxSize) { - super(maxSize); - mCachedLocales = cachedLocales; - } - - @Override - protected void entryRemoved(boolean evicted, Locale key, - DictionaryFacilitator oldValue, DictionaryFacilitator newValue) { - if (oldValue != null && oldValue != newValue) { - oldValue.closeDictionaries(); - } - if (key != null && newValue == null) { - // Remove locale from the cache when the dictionary facilitator for the locale is - // evicted and new facilitator is not set for the locale. - mCachedLocales.remove(key); - if (size() >= maxSize()) { - Log.w(TAG, "DictionaryFacilitator for " + key.toString() - + " has been evicted due to cache size limit." - + " size: " + size() + ", maxSize: " + maxSize()); - } - } - } - } - private static final int MAX_DICTIONARY_FACILITATOR_COUNT = 3; - private final LruCache<Locale, DictionaryFacilitator> mDictionaryFacilitatorCache = - new DictionaryFacilitatorLruCache(mCachedLocales, MAX_DICTIONARY_FACILITATOR_COUNT); + private final DictionaryFacilitatorLruCache mDictionaryFacilitatorCache = + new DictionaryFacilitatorLruCache(this /* context */, MAX_DICTIONARY_FACILITATOR_COUNT, + DICTIONARY_NAME_PREFIX); private final ConcurrentHashMap<Locale, Keyboard> mKeyboardCache = new ConcurrentHashMap<>(); // The threshold for a suggestion to be considered "recommended". private float mRecommendedThreshold; - // Whether to use the contacts dictionary - private boolean mUseContactsDictionary; // TODO: make a spell checker option to block offensive words or not private final SettingsValuesForSuggestion mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */, true /* spaceAwareGestureEnabled */, null /* additionalFeaturesSettingValues */); - private final Object mDictionaryLock = new Object(); public static final String SINGLE_QUOTE = "\u0027"; public static final String APOSTROPHE = "\u2019"; @@ -176,20 +122,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { if (!PREF_USE_CONTACTS_KEY.equals(key)) return; final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); - if (useContactsDictionary != mUseContactsDictionary) { - mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); - try { - mUseContactsDictionary = useContactsDictionary; - for (final Locale locale : mCachedLocales) { - final DictionaryFacilitator dictionaryFacilitator = - mDictionaryFacilitatorCache.get(locale); - resetDictionariesForLocale(this /* context */, - dictionaryFacilitator, locale, mUseContactsDictionary); - } - } finally { - mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); - } - } + mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary); } @Override @@ -222,7 +155,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService mSemaphore.acquireUninterruptibly(); try { DictionaryFacilitator dictionaryFacilitatorForLocale = - getDictionaryFacilitatorForLocaleLocked(locale); + mDictionaryFacilitatorCache.get(locale); return dictionaryFacilitatorForLocale.isValidWord(word, false /* igroreCase */); } finally { mSemaphore.release(); @@ -236,7 +169,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService try { sessionId = mSessionIdPool.poll(); DictionaryFacilitator dictionaryFacilitatorForLocale = - getDictionaryFacilitatorForLocaleLocked(locale); + mDictionaryFacilitatorCache.get(locale); return dictionaryFacilitatorForLocale.getSuggestionResults(composer, prevWordsInfo, proximityInfo, mSettingsValuesForSuggestion, sessionId); } finally { @@ -251,56 +184,18 @@ public final class AndroidSpellCheckerService extends SpellCheckerService mSemaphore.acquireUninterruptibly(); try { final DictionaryFacilitator dictionaryFacilitator = - getDictionaryFacilitatorForLocaleLocked(locale); + mDictionaryFacilitatorCache.get(locale); return dictionaryFacilitator.hasInitializedMainDictionary(); } finally { mSemaphore.release(); } } - private DictionaryFacilitator getDictionaryFacilitatorForLocaleLocked(final Locale locale) { - DictionaryFacilitator dictionaryFacilitatorForLocale = - mDictionaryFacilitatorCache.get(locale); - if (dictionaryFacilitatorForLocale == null) { - dictionaryFacilitatorForLocale = new DictionaryFacilitator(); - mDictionaryFacilitatorCache.put(locale, dictionaryFacilitatorForLocale); - mCachedLocales.add(locale); - resetDictionariesForLocale(this /* context */, dictionaryFacilitatorForLocale, - locale, mUseContactsDictionary); - } - return dictionaryFacilitatorForLocale; - } - - private static void resetDictionariesForLocale(final Context context, - final DictionaryFacilitator dictionaryFacilitator, final Locale locale, - final boolean useContactsDictionary) { - dictionaryFacilitator.resetDictionariesWithDictNamePrefix(context, locale, - useContactsDictionary, false /* usePersonalizedDicts */, - false /* forceReloadMainDictionary */, null /* listener */, - DICTIONARY_NAME_PREFIX); - for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) { - try { - dictionaryFacilitator.waitForLoadingMainDictionary( - WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS); - return; - } catch (final InterruptedException e) { - Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e); - if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) { - Log.i(TAG, "Retry", e); - } else { - Log.w(TAG, "Give up retrying. Retried " - + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e); - } - } - } - } - @Override public boolean onUnbind(final Intent intent) { mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); try { mDictionaryFacilitatorCache.evictAll(); - mCachedLocales.clear(); } finally { mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); } @@ -334,7 +229,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(this, editorInfo); builder.setKeyboardGeometry( SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT); - builder.setSubtype(subtype); + builder.setSubtype(new RichInputMethodSubtype(subtype)); builder.setIsSpellChecker(true /* isSpellChecker */); builder.disableTouchPositionCorrectionData(); return builder.build(); diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java index 936219332..02f1c5f00 100644 --- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java @@ -213,12 +213,22 @@ public final class CapsModeUtils { 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; + // end of a sentence. If we have a sentence terminator (typically a question mark or an + // exclamation mark), then it's the end of a sentence; however, we treat the abbreviation + // marker specially because usually is the same char as the sentence separator (the + // period in most languages) and in this case we need to apply a heuristic to determine + // in which of these senses it's used. + if (spacingAndPunctuations.isSentenceTerminator(c) + && !spacingAndPunctuations.isAbbreviationMarker(c)) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS + | TextUtils.CAP_MODE_SENTENCES) & reqModes; } + // If we reach here, we know we have whitespace before the cursor and before that there + // is something that either does not terminate the sentence, or a symbol preceded by the + // start of the text, or it's the sentence separator AND it happens to be the same code + // point as the abbreviation marker. + // If it's a symbol or something that does not terminate the sentence, then we need to + // return caps for MODE_CHARACTERS and MODE_WORDS, but not for MODE_SENTENCES. if (!spacingAndPunctuations.isSentenceSeparator(c) || j <= 0) { return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; } diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java index 787e4a59d..94c62429e 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java @@ -36,10 +36,38 @@ public interface DistracterFilter { public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo, final String testedWord, final Locale locale); + public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, final String testedWord, + final Locale locale); + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes); public void close(); + public static final class HandlingType { + private final static int REQUIRE_NO_SPECIAL_HANDLINGS = 0x0; + private final static int SHOULD_BE_LOWER_CASED = 0x1; + private final static int SHOULD_BE_HANDLED_AS_OOV = 0x2; + + public static int getHandlingType(final boolean shouldBeLowerCased, final boolean isOov) { + int wordHandlingType = HandlingType.REQUIRE_NO_SPECIAL_HANDLINGS; + if (shouldBeLowerCased) { + wordHandlingType |= HandlingType.SHOULD_BE_LOWER_CASED; + } + if (isOov) { + wordHandlingType |= HandlingType.SHOULD_BE_HANDLED_AS_OOV; + } + return wordHandlingType; + } + + public static boolean shouldBeLowerCased(final int handlingType) { + return (handlingType & SHOULD_BE_LOWER_CASED) != 0; + } + + public static boolean shouldBeHandledAsOov(final int handlingType) { + return (handlingType & SHOULD_BE_HANDLED_AS_OOV) != 0; + } + }; + public static final DistracterFilter EMPTY_DISTRACTER_FILTER = new DistracterFilter() { @Override public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo, @@ -48,6 +76,12 @@ public interface DistracterFilter { } @Override + public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, + final String testedWord, final Locale locale) { + return HandlingType.REQUIRE_NO_SPECIAL_HANDLINGS; + } + + @Override public void close() { } diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java index 27973287d..1db525502 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java @@ -20,13 +20,14 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.ConcurrentHashMap; import android.content.Context; import android.content.res.Resources; import android.text.InputType; import android.util.Log; import android.util.LruCache; +import android.util.Pair; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; @@ -34,7 +35,9 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.latin.DictionaryFacilitator; +import com.android.inputmethod.latin.DictionaryFacilitatorLruCache; import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; @@ -48,15 +51,16 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr DistracterFilterCheckingExactMatchesAndSuggestions.class.getSimpleName(); private static final boolean DEBUG = false; - private static final long TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS = 120; - private static final int MAX_DISTRACTERS_CACHE_SIZE = 512; + private static final int MAX_DICTIONARY_FACILITATOR_CACHE_SIZE = 3; + private static final int MAX_DISTRACTERS_CACHE_SIZE = 1024; private final Context mContext; - private final Map<Locale, InputMethodSubtype> mLocaleToSubtypeMap; - private final Map<Locale, Keyboard> mLocaleToKeyboardMap; - private final DictionaryFacilitator mDictionaryFacilitator; - private final LruCache<String, Boolean> mDistractersCache; - private Keyboard mKeyboard; + private final ConcurrentHashMap<Locale, InputMethodSubtype> mLocaleToSubtypeCache; + private final ConcurrentHashMap<Locale, Keyboard> mLocaleToKeyboardCache; + private final DictionaryFacilitatorLruCache mDictionaryFacilitatorLruCache; + // The key is a pair of a locale and a word. The value indicates the word is a distracter to + // words of the locale. + private final LruCache<Pair<Locale, String>, Boolean> mDistractersCache; private final Object mLock = new Object(); // If the score of the top suggestion exceeds this value, the tested word (e.g., @@ -73,16 +77,19 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr */ public DistracterFilterCheckingExactMatchesAndSuggestions(final Context context) { mContext = context; - mLocaleToSubtypeMap = new HashMap<>(); - mLocaleToKeyboardMap = new HashMap<>(); - mDictionaryFacilitator = new DictionaryFacilitator(); + mLocaleToSubtypeCache = new ConcurrentHashMap<>(); + mLocaleToKeyboardCache = new ConcurrentHashMap<>(); + mDictionaryFacilitatorLruCache = new DictionaryFacilitatorLruCache(context, + MAX_DICTIONARY_FACILITATOR_CACHE_SIZE, "" /* dictionaryNamePrefix */); mDistractersCache = new LruCache<>(MAX_DISTRACTERS_CACHE_SIZE); - mKeyboard = null; } @Override public void close() { - mDictionaryFacilitator.closeDictionaries(); + mLocaleToSubtypeCache.clear(); + mLocaleToKeyboardCache.clear(); + mDictionaryFacilitatorLruCache.evictAll(); + // Don't clear mDistractersCache. } @Override @@ -99,29 +106,36 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr newLocaleToSubtypeMap.put(locale, subtype); } } - if (mLocaleToSubtypeMap.equals(newLocaleToSubtypeMap)) { + if (mLocaleToSubtypeCache.equals(newLocaleToSubtypeMap)) { // Enabled subtypes have not been changed. return; } - synchronized (mLock) { - mLocaleToSubtypeMap.clear(); - mLocaleToSubtypeMap.putAll(newLocaleToSubtypeMap); - mLocaleToKeyboardMap.clear(); + // Update subtype and keyboard map for locales that are in the current mapping. + for (final Locale locale: mLocaleToSubtypeCache.keySet()) { + if (newLocaleToSubtypeMap.containsKey(locale)) { + final InputMethodSubtype newSubtype = newLocaleToSubtypeMap.remove(locale); + if (newSubtype.equals(newLocaleToSubtypeMap.get(locale))) { + // Mapping has not been changed. + continue; + } + mLocaleToSubtypeCache.replace(locale, newSubtype); + } else { + mLocaleToSubtypeCache.remove(locale); + } + mLocaleToKeyboardCache.remove(locale); } + // Add locales that are not in the current mapping. + mLocaleToSubtypeCache.putAll(newLocaleToSubtypeMap); } - private void loadKeyboardForLocale(final Locale newLocale) { - final Keyboard cachedKeyboard = mLocaleToKeyboardMap.get(newLocale); + private Keyboard getKeyboardForLocale(final Locale locale) { + final Keyboard cachedKeyboard = mLocaleToKeyboardCache.get(locale); if (cachedKeyboard != null) { - mKeyboard = cachedKeyboard; - return; - } - final InputMethodSubtype subtype; - synchronized (mLock) { - subtype = mLocaleToSubtypeMap.get(newLocale); + return cachedKeyboard; } + final InputMethodSubtype subtype = mLocaleToSubtypeCache.get(locale); if (subtype == null) { - return; + return null; } final EditorInfo editorInfo = new EditorInfo(); editorInfo.inputType = InputType.TYPE_CLASS_TEXT; @@ -131,18 +145,12 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res); final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res); builder.setKeyboardGeometry(keyboardWidth, keyboardHeight); - builder.setSubtype(subtype); + builder.setSubtype(new RichInputMethodSubtype(subtype)); builder.setIsSpellChecker(false /* isSpellChecker */); final KeyboardLayoutSet layoutSet = builder.build(); - mKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); - } - - private void loadDictionariesForLocale(final Locale newlocale) throws InterruptedException { - mDictionaryFacilitator.resetDictionaries(mContext, newlocale, - false /* useContactsDict */, false /* usePersonalizedDicts */, - false /* forceReloadMainDictionary */, null /* listener */); - mDictionaryFacilitator.waitForLoadingMainDictionary( - TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS, TimeUnit.SECONDS); + final Keyboard newKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); + mLocaleToKeyboardCache.put(locale, newKeyboard); + return newKeyboard; } /** @@ -160,30 +168,18 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr if (locale == null) { return false; } - if (!locale.equals(mDictionaryFacilitator.getLocale())) { - synchronized (mLock) { - if (!mLocaleToSubtypeMap.containsKey(locale)) { - Log.e(TAG, "Locale " + locale + " is not enabled."); - // TODO: Investigate what we should do for disabled locales. - return false; - } - loadKeyboardForLocale(locale); - // Reset dictionaries for the locale. - try { - mDistractersCache.evictAll(); - loadDictionariesForLocale(locale); - } catch (final InterruptedException e) { - Log.e(TAG, "Interrupted while waiting for loading dicts in DistracterFilter", - e); - return false; - } - } + if (!mLocaleToSubtypeCache.containsKey(locale)) { + Log.e(TAG, "Locale " + locale + " is not enabled."); + // TODO: Investigate what we should do for disabled locales. + return false; } - + final DictionaryFacilitator dictionaryFacilitator = + mDictionaryFacilitatorLruCache.get(locale); if (DEBUG) { Log.d(TAG, "testedWord: " + testedWord); } - final Boolean isCachedDistracter = mDistractersCache.get(testedWord); + final Pair<Locale, String> cacheKey = new Pair<>(locale, testedWord); + final Boolean isCachedDistracter = mDistractersCache.get(cacheKey); if (isCachedDistracter != null && isCachedDistracter) { if (DEBUG) { Log.d(TAG, "isDistracter: true (cache hit)"); @@ -192,15 +188,14 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr } final boolean isDistracterCheckedByGetMaxFreqencyOfExactMatches = - checkDistracterUsingMaxFreqencyOfExactMatches(testedWord); + checkDistracterUsingMaxFreqencyOfExactMatches(dictionaryFacilitator, testedWord); if (isDistracterCheckedByGetMaxFreqencyOfExactMatches) { - // Add the word to the cache. - mDistractersCache.put(testedWord, Boolean.TRUE); + // Add the pair of locale and word to the cache. + mDistractersCache.put(cacheKey, Boolean.TRUE); return true; } - final boolean isValidWord = mDictionaryFacilitator.isValidWord(testedWord, - false /* ignoreCase */); - if (isValidWord) { + final boolean Word = dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */); + if (Word) { // Valid word is not a distractor. if (DEBUG) { Log.d(TAG, "isDistracter: false (valid word)"); @@ -208,21 +203,23 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr return false; } + final Keyboard keyboard = getKeyboardForLocale(locale); final boolean isDistracterCheckedByGetSuggestion = - checkDistracterUsingGetSuggestions(testedWord); + checkDistracterUsingGetSuggestions(dictionaryFacilitator, keyboard, testedWord); if (isDistracterCheckedByGetSuggestion) { - // Add the word to the cache. - mDistractersCache.put(testedWord, Boolean.TRUE); + // Add the pair of locale and word to the cache. + mDistractersCache.put(cacheKey, Boolean.TRUE); return true; } return false; } - private boolean checkDistracterUsingMaxFreqencyOfExactMatches(final String testedWord) { + private static boolean checkDistracterUsingMaxFreqencyOfExactMatches( + final DictionaryFacilitator dictionaryFacilitator, final String testedWord) { // The tested word is a distracter when there is a word that is exact matched to the tested // word and its probability is higher than the tested word's probability. - final int perfectMatchFreq = mDictionaryFacilitator.getFrequency(testedWord); - final int exactMatchFreq = mDictionaryFacilitator.getMaxFrequencyOfExactMatches(testedWord); + final int perfectMatchFreq = dictionaryFacilitator.getFrequency(testedWord); + final int exactMatchFreq = dictionaryFacilitator.getMaxFrequencyOfExactMatches(testedWord); final boolean isDistracter = perfectMatchFreq < exactMatchFreq; if (DEBUG) { Log.d(TAG, "perfectMatchFreq: " + perfectMatchFreq); @@ -232,8 +229,10 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr return isDistracter; } - private boolean checkDistracterUsingGetSuggestions(final String testedWord) { - if (mKeyboard == null) { + private boolean checkDistracterUsingGetSuggestions( + final DictionaryFacilitator dictionaryFacilitator, final Keyboard keyboard, + final String testedWord) { + if (keyboard == null) { return false; } final SettingsValuesForSuggestion settingsValuesForSuggestion = @@ -246,24 +245,24 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr testedWord; final WordComposer composer = new WordComposer(); final int[] codePoints = StringUtils.toCodePointArray(testedWord); - + final int[] coordinates = keyboard.getCoordinates(codePoints); + composer.setComposingWord(codePoints, coordinates); + final SuggestionResults suggestionResults; synchronized (mLock) { - final int[] coordinates = mKeyboard.getCoordinates(codePoints); - composer.setComposingWord(codePoints, coordinates); - final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( - composer, PrevWordsInfo.EMPTY_PREV_WORDS_INFO, mKeyboard.getProximityInfo(), + suggestionResults = dictionaryFacilitator.getSuggestionResults( + composer, PrevWordsInfo.EMPTY_PREV_WORDS_INFO, keyboard.getProximityInfo(), settingsValuesForSuggestion, 0 /* sessionId */); - if (suggestionResults.isEmpty()) { - return false; - } - final SuggestedWordInfo firstSuggestion = suggestionResults.first(); - final boolean isDistractor = suggestionExceedsDistracterThreshold( - firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD); - if (DEBUG) { - Log.d(TAG, "isDistracter: " + isDistractor); - } - return isDistractor; } + if (suggestionResults.isEmpty()) { + return false; + } + final SuggestedWordInfo firstSuggestion = suggestionResults.first(); + final boolean isDistractor = suggestionExceedsDistracterThreshold( + firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD); + if (DEBUG) { + Log.d(TAG, "isDistracter: " + isDistractor); + } + return isDistractor; } private static boolean suggestionExceedsDistracterThreshold(final SuggestedWordInfo suggestion, @@ -283,4 +282,41 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr } return false; } + + private boolean shouldBeLowerCased(final PrevWordsInfo prevWordsInfo, final String testedWord, + final Locale locale) { + final DictionaryFacilitator dictionaryFacilitator = + mDictionaryFacilitatorLruCache.get(locale); + if (dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */)) { + return false; + } + final String lowerCaseTargetWord = testedWord.toLowerCase(locale); + if (testedWord.equals(lowerCaseTargetWord)) { + return false; + } + if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) { + return true; + } + if (StringUtils.getCapitalizationType(testedWord) == StringUtils.CAPITALIZE_FIRST + && !prevWordsInfo.isValid()) { + // TODO: Check beginning-of-sentence. + return true; + } + return false; + } + + @Override + public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, final String testedWord, + final Locale locale) { + // TODO: Use this method for user history dictionary. + if (testedWord == null|| locale == null) { + return HandlingType.getHandlingType(false /* shouldBeLowerCased */, false /* isOov */); + } + final boolean shouldBeLowerCased = shouldBeLowerCased(prevWordsInfo, testedWord, locale); + final String caseModifiedWord = + shouldBeLowerCased ? testedWord.toLowerCase(locale) : testedWord; + final boolean isOov = !mDictionaryFacilitatorLruCache.get(locale).isValidWord( + caseModifiedWord, false /* ignoreCase */); + return HandlingType.getHandlingType(shouldBeLowerCased, isOov); + } } diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java index 4ad4ba784..349236f18 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java @@ -48,6 +48,12 @@ public class DistracterFilterCheckingIsInDictionary implements DistracterFilter } @Override + public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, final String testedWord, + final Locale locale) { + return mDistracterFilter.getWordHandlingType(prevWordsInfo, testedWord, locale); + } + + @Override public void updateEnabledSubtypes(List<InputMethodSubtype> enabledSubtypes) { // Do nothing. } diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java index fbce3f2fd..05d124764 100644 --- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java +++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java @@ -22,6 +22,7 @@ import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryFacilitator; import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.utils.DistracterFilter.HandlingType; import java.util.ArrayList; import java.util.List; @@ -81,8 +82,7 @@ public final class LanguageModelParam { // Process a list of words and return a list of {@link LanguageModelParam} objects. public static ArrayList<LanguageModelParam> createLanguageModelParamsFrom( final List<String> tokens, final int timestamp, - final DictionaryFacilitator dictionaryFacilitator, - final SpacingAndPunctuations spacingAndPunctuations, + final SpacingAndPunctuations spacingAndPunctuations, final Locale locale, final DistracterFilter distracterFilter) { final ArrayList<LanguageModelParam> languageModelParams = new ArrayList<>(); final int N = tokens.size(); @@ -111,8 +111,7 @@ public final class LanguageModelParam { } final LanguageModelParam languageModelParam = detectWhetherVaildWordOrNotAndGetLanguageModelParam( - prevWordsInfo, tempWord, timestamp, dictionaryFacilitator, - distracterFilter); + prevWordsInfo, tempWord, timestamp, locale, distracterFilter); if (languageModelParam == null) { continue; } @@ -125,47 +124,25 @@ public final class LanguageModelParam { private static LanguageModelParam detectWhetherVaildWordOrNotAndGetLanguageModelParam( final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp, - final DictionaryFacilitator dictionaryFacilitator, - final DistracterFilter distracterFilter) { - final Locale locale = dictionaryFacilitator.getLocale(); + final Locale locale, final DistracterFilter distracterFilter) { if (locale == null) { return null; } - if (dictionaryFacilitator.isValidWord(targetWord, false /* ignoreCase */)) { - return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp, - true /* isValidWord */, locale, distracterFilter); - } - - final String lowerCaseTargetWord = targetWord.toLowerCase(locale); - if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) { - // Add the lower-cased word. - return createAndGetLanguageModelParamOfWord(prevWordsInfo, lowerCaseTargetWord, - timestamp, true /* isValidWord */, locale, distracterFilter); + final int wordHandlingType = distracterFilter.getWordHandlingType(prevWordsInfo, + targetWord, locale); + final String word = HandlingType.shouldBeLowerCased(wordHandlingType) ? + targetWord.toLowerCase(locale) : targetWord; + if (distracterFilter.isDistracterToWordsInDictionaries(prevWordsInfo, targetWord, locale)) { + // The word is a distracter. + return null; } - - // Treat the word as an OOV word. - return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp, - false /* isValidWord */, locale, distracterFilter); + return createAndGetLanguageModelParamOfWord(prevWordsInfo, word, timestamp, + !HandlingType.shouldBeHandledAsOov(wordHandlingType)); } private static LanguageModelParam createAndGetLanguageModelParamOfWord( - final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp, - final boolean isValidWord, final Locale locale, - final DistracterFilter distracterFilter) { - final String word; - if (StringUtils.getCapitalizationType(targetWord) == StringUtils.CAPITALIZE_FIRST - && !prevWordsInfo.isValid() && !isValidWord) { - word = targetWord.toLowerCase(locale); - } else { - word = targetWord; - } - // Check whether the word is a distracter to words in the dictionaries. - if (distracterFilter.isDistracterToWordsInDictionaries(prevWordsInfo, word, locale)) { - if (DEBUG) { - Log.d(TAG, "The word (" + word + ") is a distracter. Skip this word."); - } - return null; - } + final PrevWordsInfo prevWordsInfo, final String word, final int timestamp, + final boolean isValidWord) { final int unigramProbability = isValidWord ? UNIGRAM_PROBABILITY_FOR_VALID_WORD : UNIGRAM_PROBABILITY_FOR_OOV_WORD; if (!prevWordsInfo.isValid()) { diff --git a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java b/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java deleted file mode 100644 index 1ca895fdb..000000000 --- a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import android.view.inputmethod.InputMethodSubtype; - -public final class SpacebarLanguageUtils { - private SpacebarLanguageUtils() { - // Intentional empty constructor for utility class. - } - - // InputMethodSubtype's display name for spacebar text in its locale. - // isAdditionalSubtype (T=true, F=false) - // locale layout | Middle Full - // ------ ------- - --------- ---------------------- - // en_US qwerty F English English (US) exception - // en_GB qwerty F English English (UK) exception - // es_US spanish F Español Español (EE.UU.) exception - // fr azerty F Français Français - // fr_CA qwerty F Français Français (Canada) - // fr_CH swiss F Français Français (Suisse) - // de qwertz F Deutsch Deutsch - // de_CH swiss T Deutsch Deutsch (Schweiz) - // zz qwerty F QWERTY QWERTY - // fr qwertz T Français Français - // de qwerty T Deutsch Deutsch - // en_US azerty T English English (US) - // zz azerty T AZERTY AZERTY - // Get InputMethodSubtype's full display name in its locale. - public static String getFullDisplayName(final InputMethodSubtype subtype) { - if (SubtypeLocaleUtils.isNoLanguage(subtype)) { - return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype); - } - return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(subtype.getLocale()); - } - - // Get InputMethodSubtype's middle display name in its locale. - public static String getMiddleDisplayName(final InputMethodSubtype subtype) { - if (SubtypeLocaleUtils.isNoLanguage(subtype)) { - return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype); - } - return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(subtype.getLocale()); - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index 38f0b3fee..55557de9d 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -41,9 +41,9 @@ public final class StringUtils { // This utility class is not publicly instantiable. } - public static int codePointCount(final String text) { + public static int codePointCount(final CharSequence text) { if (TextUtils.isEmpty(text)) return 0; - return text.codePointCount(0, text.length()); + return Character.codePointCount(text, 0, text.length()); } public static String newSingleCodePointString(int codePoint) { diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java index 351d01400..96a6510fc 100644 --- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java @@ -27,11 +27,17 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.RichInputMethodSubtype; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; +/** + * A helper class to deal with subtype locales. + */ +// TODO: consolidate this into RichInputMethodSubtype public final class SubtypeLocaleUtils { private static final String TAG = SubtypeLocaleUtils.class.getSimpleName(); @@ -52,6 +58,8 @@ public final class SubtypeLocaleUtils { private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap = new HashMap<>(); // Keyboard layout to subtype name resource id map. private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap = new HashMap<>(); + // Exceptional locale whose name should be displayed in Locale.ROOT. + static final HashSet<String> sExceptionalLocaleDisplayedInRootLocale = new HashSet<>(); // Exceptional locale to subtype name resource id map. private static final HashMap<String, Integer> sExceptionalLocaleToNameIdsMap = new HashMap<>(); // Exceptional locale to subtype name with layout resource id map. @@ -106,6 +114,12 @@ public final class SubtypeLocaleUtils { sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId); } + final String[] exceptionalLocaleInRootLocale = res.getStringArray( + R.array.subtype_locale_displayed_in_root_locale); + for (int i = 0; i < exceptionalLocaleInRootLocale.length; i++) { + sExceptionalLocaleDisplayedInRootLocale.add(exceptionalLocaleInRootLocale[i]); + } + final String[] exceptionalLocales = res.getStringArray( R.array.subtype_locale_exception_keys); for (int i = 0; i < exceptionalLocales.length; i++) { @@ -157,6 +171,9 @@ public final class SubtypeLocaleUtils { if (NO_LANGUAGE.equals(localeString)) { return sResources.getConfiguration().locale; } + if (sExceptionalLocaleDisplayedInRootLocale.contains(localeString)) { + return Locale.ROOT; + } return LocaleUtils.constructLocaleFromString(localeString); } @@ -171,9 +188,15 @@ public final class SubtypeLocaleUtils { } public static String getSubtypeLanguageDisplayName(final String localeString) { - final Locale locale = LocaleUtils.constructLocaleFromString(localeString); final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString); - return getSubtypeLocaleDisplayNameInternal(locale.getLanguage(), displayLocale); + final String languageString; + if (sExceptionalLocaleDisplayedInRootLocale.contains(localeString)) { + languageString = localeString; + } else { + final Locale locale = LocaleUtils.constructLocaleFromString(localeString); + languageString = locale.getLanguage(); + } + return getSubtypeLocaleDisplayNameInternal(languageString, displayLocale); } private static String getSubtypeLocaleDisplayNameInternal(final String localeString, @@ -242,6 +265,7 @@ public final class SubtypeLocaleUtils { private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype, final Locale displayLocale) { final String replacementString = getReplacementString(subtype, displayLocale); + // TODO: rework this for multi-lingual subtypes final int nameResId = subtype.getNameResId(); final RunInLocale<String> getSubtypeName = new RunInLocale<String>() { @Override @@ -264,12 +288,14 @@ public final class SubtypeLocaleUtils { getSubtypeName.runInLocale(sResources, displayLocale), displayLocale); } - public static boolean isNoLanguage(final InputMethodSubtype subtype) { + public static Locale getSubtypeLocale(final InputMethodSubtype subtype) { final String localeString = subtype.getLocale(); - return NO_LANGUAGE.equals(localeString); + return LocaleUtils.constructLocaleFromString(localeString); } - public static Locale getSubtypeLocale(final InputMethodSubtype subtype) { + // TODO: remove this. When RichInputMethodSubtype#getLocale is removed we can do away with this + // method at the same time. + public static Locale getSubtypeLocale(final RichInputMethodSubtype subtype) { final String localeString = subtype.getLocale(); return LocaleUtils.constructLocaleFromString(localeString); } @@ -283,6 +309,10 @@ public final class SubtypeLocaleUtils { return sKeyboardLayoutToDisplayNameMap.get(layoutName); } + public static String getKeyboardLayoutSetName(final RichInputMethodSubtype subtype) { + return getKeyboardLayoutSetName(subtype.getRawSubtype()); + } + public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) { String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET); if (keyboardLayoutSet == null) { @@ -318,7 +348,7 @@ public final class SubtypeLocaleUtils { return Arrays.binarySearch(SORTED_RTL_LANGUAGES, language) >= 0; } - public static boolean isRtlLanguage(final InputMethodSubtype subtype) { + public static boolean isRtlLanguage(final RichInputMethodSubtype subtype) { return isRtlLanguage(getSubtypeLocale(subtype)); } diff --git a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java index 7170bd789..eaa5743d4 100644 --- a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java +++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java @@ -30,18 +30,16 @@ import java.util.TreeSet; * than its limit */ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { - public final Locale mLocale; public final ArrayList<SuggestedWordInfo> mRawSuggestions; private final int mCapacity; - public SuggestionResults(final Locale locale, final int capacity) { - this(locale, sSuggestedWordInfoComparator, capacity); + public SuggestionResults(final int capacity) { + this(sSuggestedWordInfoComparator, capacity); } - public SuggestionResults(final Locale locale, final Comparator<SuggestedWordInfo> comparator, + public SuggestionResults(final Comparator<SuggestedWordInfo> comparator, final int capacity) { super(comparator); - mLocale = locale; mCapacity = capacity; if (ProductionFlags.INCLUDE_RAW_SUGGESTIONS) { mRawSuggestions = new ArrayList<>(); |