diff options
Diffstat (limited to 'java/src')
24 files changed, 324 insertions, 549 deletions
diff --git a/java/src/com/android/inputmethod/compat/DownloadManagerCompatUtils.java b/java/src/com/android/inputmethod/compat/DownloadManagerCompatUtils.java new file mode 100644 index 000000000..6209b60b3 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/DownloadManagerCompatUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.compat; + +import android.app.DownloadManager; + +import java.lang.reflect.Method; + +public final class DownloadManagerCompatUtils { + // DownloadManager.Request#setAllowedOverMetered() has been introduced + // in API level 16 (Build.VERSION_CODES.JELLY_BEAN). + private static final Method METHOD_setAllowedOverMetered = CompatUtils.getMethod( + DownloadManager.Request.class, "setAllowedOverMetered", boolean.class); + + public static DownloadManager.Request setAllowedOverMetered( + final DownloadManager.Request request, final boolean allowOverMetered) { + return (DownloadManager.Request)CompatUtils.invoke(request, + request /* default return value */, METHOD_setAllowedOverMetered, allowOverMetered); + } + + public static final boolean hasSetAllowedOverMetered() { + return null != METHOD_setAllowedOverMetered; + } +} diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java index 1b526d453..09f8032cc 100644 --- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java +++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java @@ -25,9 +25,8 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; -import com.android.inputmethod.latin.BinaryDictionaryFileDumper; +import com.android.inputmethod.compat.DownloadManagerCompatUtils; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.common.LocaleUtils; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.DebugLogUtils; @@ -85,7 +84,7 @@ public final class ActionBatch { * Execute this action NOW. * @param context the context to get system services, resources, databases */ - void execute(final Context context); + public void execute(final Context context); } /** @@ -97,10 +96,13 @@ public final class ActionBatch { private final String mClientId; // The data to download. May not be null. final WordListMetadata mWordList; - public StartDownloadAction(final String clientId, final WordListMetadata wordList) { + final boolean mForceStartNow; + public StartDownloadAction(final String clientId, + final WordListMetadata wordList, final boolean forceStartNow) { DebugLogUtils.l("New download action for client ", clientId, " : ", wordList); mClientId = clientId; mWordList = wordList; + mForceStartNow = forceStartNow; } @Override @@ -139,9 +141,32 @@ public final class ActionBatch { final Request request = new Request(uri); final Resources res = context.getResources(); - request.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE); + if (!mForceStartNow) { + if (DownloadManagerCompatUtils.hasSetAllowedOverMetered()) { + final boolean allowOverMetered; + switch (UpdateHandler.getDownloadOverMeteredSetting(context)) { + case UpdateHandler.DOWNLOAD_OVER_METERED_DISALLOWED: + // User said no: don't allow. + allowOverMetered = false; + break; + case UpdateHandler.DOWNLOAD_OVER_METERED_ALLOWED: + // User said yes: allow. + allowOverMetered = true; + break; + default: // UpdateHandler.DOWNLOAD_OVER_METERED_SETTING_UNKNOWN + // Don't know: use the default value from configuration. + allowOverMetered = res.getBoolean(R.bool.allow_over_metered); + } + DownloadManagerCompatUtils.setAllowedOverMetered(request, allowOverMetered); + } else { + request.setAllowedNetworkTypes(Request.NETWORK_WIFI); + } + request.setAllowedOverRoaming(res.getBoolean(R.bool.allow_over_roaming)); + } // if mForceStartNow, then allow all network types and roaming, which is the default. request.setTitle(mWordList.mDescription); - request.setNotificationVisibility(Request.VISIBILITY_HIDDEN); + request.setNotificationVisibility( + res.getBoolean(R.bool.display_notification_for_auto_update) + ? Request.VISIBILITY_VISIBLE : Request.VISIBILITY_HIDDEN); request.setVisibleInDownloadsUi( res.getBoolean(R.bool.dict_downloads_visible_in_download_UI)); @@ -185,17 +210,9 @@ public final class ActionBatch { + " for an InstallAfterDownload action. Bailing out."); return; } - DebugLogUtils.l("Setting word list as installed"); final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues); - - // Install the downloaded file by un-compressing and moving it to the staging - // directory. Ideally, we should do this before updating the DB, but the - // installDictToStagingFromContentProvider() relies on the db being updated. - final String localeString = mWordListValues.getAsString(MetadataDbHelper.LOCALE_COLUMN); - BinaryDictionaryFileDumper.installDictToStagingFromContentProvider( - LocaleUtils.constructLocaleFromString(localeString), context, false); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java b/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java index 3d0e29ed0..3cd822a3c 100644 --- a/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java +++ b/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java @@ -22,6 +22,8 @@ import android.content.SharedPreferences; public final class CommonPreferences { private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"; + public static final String PREF_FORCE_DOWNLOAD_DICT = "pref_key_force_download_dict"; + public static SharedPreferences getCommonPreferences(final Context context) { return context.getSharedPreferences(COMMON_PREFERENCES_NAME, 0); } @@ -37,4 +39,14 @@ public final class CommonPreferences { editor.putBoolean(id, false); editor.apply(); } + + public static boolean isForceDownloadDict(Context context) { + return getCommonPreferences(context).getBoolean(PREF_FORCE_DOWNLOAD_DICT, false); + } + + public static void setForceDownloadDict(Context context, boolean forceDownload) { + SharedPreferences.Editor editor = getCommonPreferences(context).edit(); + editor.putBoolean(PREF_FORCE_DOWNLOAD_DICT, forceDownload); + editor.apply(); + } } diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java index 308b123e1..659fe5c51 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java @@ -243,8 +243,14 @@ public final class DictionaryProvider extends ContentProvider { // Fall through case DICTIONARY_V1_DICT_INFO: final String locale = uri.getLastPathSegment(); + // If LatinIME does not have a dictionary for this locale at all, it will + // send us true for this value. In this case, we may prompt the user for + // a decision about downloading a dictionary even over a metered connection. + final String mayPromptValue = + uri.getQueryParameter(QUERY_PARAMETER_MAY_PROMPT_USER); + final boolean mayPrompt = QUERY_PARAMETER_TRUE.equals(mayPromptValue); final Collection<WordListInfo> dictFiles = - getDictionaryWordListsForLocale(clientId, locale); + getDictionaryWordListsForLocale(clientId, locale, mayPrompt); // TODO: pass clientId to the following function DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext()); if (null != dictFiles && dictFiles.size() > 0) { @@ -337,10 +343,11 @@ public final class DictionaryProvider extends ContentProvider { * * @param clientId the ID of the client requesting the list * @param locale the locale for which we want the list, as a String + * @param mayPrompt true if we are allowed to prompt the user for arbitration via notification * @return a collection of ids. It is guaranteed to be non-null, but may be empty. */ private Collection<WordListInfo> getDictionaryWordListsForLocale(final String clientId, - final String locale) { + final String locale, final boolean mayPrompt) { final Context context = getContext(); final Cursor results = MetadataDbHelper.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context, @@ -405,7 +412,8 @@ public final class DictionaryProvider extends ContentProvider { } } else if (MetadataDbHelper.STATUS_AVAILABLE == wordListStatus) { // The locale is the id for the main dictionary. - UpdateHandler.installIfNeverRequested(context, clientId, wordListId); + UpdateHandler.installIfNeverRequested(context, clientId, wordListId, + mayPrompt); continue; } final WordListInfo currentBestMatch = dicts.get(wordListCategory); diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java index fe988ac70..bbdf2a380 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java @@ -192,22 +192,27 @@ public final class DictionaryService extends Service { } static void dispatchBroadcast(final Context context, final Intent intent) { - final String action = intent.getAction(); - if (DATE_CHANGED_INTENT_ACTION.equals(action)) { + if (DATE_CHANGED_INTENT_ACTION.equals(intent.getAction())) { + // Do not force download dictionaries on date change updates. + CommonPreferences.setForceDownloadDict(context, false); // This happens when the date of the device changes. This normally happens // at midnight local time, but it may happen if the user changes the date // by hand or something similar happens. checkTimeAndMaybeSetupUpdateAlarm(context); - } else if (DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION.equals(action)) { + } else if (DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION.equals(intent.getAction())) { // Intent to trigger an update now. - UpdateHandler.tryUpdate(context); - } else if (DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION.equals(action)) { + UpdateHandler.tryUpdate(context, CommonPreferences.isForceDownloadDict(context)); + } else if (DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION.equals( + intent.getAction())) { + // Enable force download of dictionaries irrespective of wifi or metered connection. + CommonPreferences.setForceDownloadDict(context, true); + // Initialize the client Db. final String mClientId = context.getString(R.string.dictionary_pack_client_id); BinaryDictionaryFileDumper.initializeClientRecordHelper(context, mClientId); // Updates the metadata and the download the dictionaries. - UpdateHandler.tryUpdate(context); + UpdateHandler.tryUpdate(context, true); } else { UpdateHandler.downloadFinished(context, intent); } @@ -258,7 +263,7 @@ public final class DictionaryService extends Service { */ public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) { if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME_MILLIS)) return; - UpdateHandler.tryUpdate(context); + UpdateHandler.tryUpdate(context, CommonPreferences.isForceDownloadDict(context)); } /** diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java index 35b46a978..88ea4e6c3 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java @@ -384,7 +384,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment // We call tryUpdate(), which returns whether we could successfully start an update. // If we couldn't, we'll never receive the end callback, so we stop the loading // animation and return to the previous screen. - if (!UpdateHandler.tryUpdate(activity)) { + if (!UpdateHandler.tryUpdate(activity, true)) { stopLoadingAnimation(); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java index 6f6b02637..3dbbc9b9b 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java +++ b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java @@ -27,8 +27,6 @@ import android.util.Log; import java.io.FileNotFoundException; -import javax.annotation.Nullable; - /** * A class to help with calling DownloadManager methods. * @@ -80,7 +78,6 @@ public class DownloadManagerWrapper { throw new FileNotFoundException(); } - @Nullable public Cursor query(final Query query) { try { if (null != mDownloadManager) { diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java b/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java index 908d931a0..91ed673ae 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java +++ b/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java @@ -80,7 +80,8 @@ public final class DownloadOverMeteredDialog extends Activity { @SuppressWarnings("unused") public void onClickAllow(final View v) { UpdateHandler.setDownloadOverMeteredSetting(this, true); - UpdateHandler.installIfNeverRequested(this, mClientId, mWordListToDownload); + UpdateHandler.installIfNeverRequested(this, mClientId, mWordListToDownload, + false /* mayPrompt */); finish(); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java index 7d01351b4..fbc899192 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java @@ -50,7 +50,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6; // The current database version. // This MUST be increased every time the dictionary pack metadata URL changes. - private static final int CURRENT_METADATA_DATABASE_VERSION = 16; + private static final int CURRENT_METADATA_DATABASE_VERSION = 14; private final static long NOT_A_DOWNLOAD_ID = -1; @@ -266,6 +266,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { */ @Override public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + // Allow automatic download of dictionaries on upgrading the database. + CommonPreferences.setForceDownloadDict(mContext, true); if (METADATA_DATABASE_INITIAL_VERSION == oldVersion && METADATA_DATABASE_VERSION_WITH_CLIENTID <= newVersion && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { @@ -343,8 +345,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper { return null != getMetadataUriAsString(context, clientId); } - private static final MetadataUriGetter sMetadataUriGetter = new MetadataUriGetter(); - /** * Returns the metadata URI as a string. * @@ -358,12 +358,13 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static String getMetadataUriAsString(final Context context, final String clientId) { SQLiteDatabase defaultDb = MetadataDbHelper.getDb(context, null); final Cursor cursor = defaultDb.query(MetadataDbHelper.CLIENT_TABLE_NAME, - new String[] { MetadataDbHelper.CLIENT_METADATA_URI_COLUMN }, + new String[] { MetadataDbHelper.CLIENT_METADATA_URI_COLUMN, + MetadataDbHelper.CLIENT_METADATA_ADDITIONAL_ID_COLUMN }, MetadataDbHelper.CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId }, null, null, null, null); try { if (!cursor.moveToFirst()) return null; - return sMetadataUriGetter.getUri(context, cursor.getString(0)); + return MetadataUriGetter.getUri(context, cursor.getString(0), cursor.getString(1)); } finally { cursor.close(); } diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index a02203d31..e61547a9d 100644 --- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java @@ -36,6 +36,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.inputmethod.compat.ConnectivityManagerCompatUtils; +import com.android.inputmethod.compat.DownloadManagerCompatUtils; import com.android.inputmethod.compat.NotificationCompatUtils; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.common.LocaleUtils; @@ -105,9 +106,9 @@ public final class UpdateHandler { * This is chiefly used by the dictionary manager UI. */ public interface UpdateEventListener { - void downloadedMetadata(boolean succeeded); - void wordListDownloadFinished(String wordListId, boolean succeeded); - void updateCycleCompleted(); + public void downloadedMetadata(boolean succeeded); + public void wordListDownloadFinished(String wordListId, boolean succeeded); + public void updateCycleCompleted(); } /** @@ -178,9 +179,10 @@ public final class UpdateHandler { /** * Download latest metadata from the server through DownloadManager for all known clients * @param context The context for retrieving resources + * @param updateNow Whether we should update NOW, or respect bandwidth policies * @return true if an update successfully started, false otherwise. */ - public static boolean tryUpdate(final Context context) { + public static boolean tryUpdate(final Context context, final boolean updateNow) { // TODO: loop through all clients instead of only doing the default one. final TreeSet<String> uris = new TreeSet<>(); final Cursor cursor = MetadataDbHelper.queryClientIds(context); @@ -206,7 +208,7 @@ public final class UpdateHandler { // it should have been rejected at the time of client registration; if there // is a bug and it happens anyway, doing nothing is the right thing to do. // For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}. - updateClientsWithMetadataUri(context, metadataUri); + updateClientsWithMetadataUri(context, updateNow, metadataUri); started = true; } } @@ -217,11 +219,12 @@ public final class UpdateHandler { * Download latest metadata from the server through DownloadManager for all relevant clients * * @param context The context for retrieving resources + * @param updateNow Whether we should update NOW, or respect bandwidth policies * @param metadataUri The client to update */ - private static void updateClientsWithMetadataUri( - final Context context, final String metadataUri) { - Log.i(TAG, "updateClientsWithMetadataUri() : MetadataUri = " + metadataUri); + private static void updateClientsWithMetadataUri(final Context context, + final boolean updateNow, final String metadataUri) { + PrivateLog.log("Update for metadata URI " + DebugLogUtils.s(metadataUri)); // Adding a disambiguator to circumvent a bug in older versions of DownloadManager. // DownloadManager also stupidly cuts the extension to replace with its own that it // gets from the content-type. We need to circumvent this. @@ -231,10 +234,25 @@ public final class UpdateHandler { DebugLogUtils.l("Request =", metadataRequest); final Resources res = context.getResources(); - metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE); + // By default, download over roaming is allowed and all network types are allowed too. + if (!updateNow) { + final boolean allowedOverMetered = res.getBoolean(R.bool.allow_over_metered); + // If we don't have to update NOW, then only do it over non-metered connections. + if (DownloadManagerCompatUtils.hasSetAllowedOverMetered()) { + DownloadManagerCompatUtils.setAllowedOverMetered(metadataRequest, + allowedOverMetered); + } else if (!allowedOverMetered) { + metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI); + } + metadataRequest.setAllowedOverRoaming(res.getBoolean(R.bool.allow_over_roaming)); + } + final boolean notificationVisible = updateNow + ? res.getBoolean(R.bool.display_notification_for_user_requested_update) + : res.getBoolean(R.bool.display_notification_for_auto_update); + metadataRequest.setTitle(res.getString(R.string.download_description)); - // Do not show the notification when downloading the metadata. - metadataRequest.setNotificationVisibility(Request.VISIBILITY_HIDDEN); + metadataRequest.setNotificationVisibility(notificationVisible + ? Request.VISIBILITY_VISIBLE : Request.VISIBILITY_HIDDEN); metadataRequest.setVisibleInDownloadsUi( res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); @@ -255,7 +273,7 @@ public final class UpdateHandler { // method will ignore it. writeMetadataDownloadId(context, metadataUri, downloadId); } - Log.i(TAG, "updateClientsWithMetadataUri() : DownloadId = " + downloadId); + PrivateLog.log("Requested download with id " + downloadId); } /** @@ -327,11 +345,11 @@ public final class UpdateHandler { */ public static long registerDownloadRequest(final DownloadManagerWrapper manager, final Request request, final SQLiteDatabase db, final String id, final int version) { - Log.i(TAG, "registerDownloadRequest() : Id = " + id + " : Version = " + version); + DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version); final long downloadId; synchronized (sSharedIdProtector) { downloadId = manager.enqueue(request); - Log.i(TAG, "registerDownloadRequest() : DownloadId = " + downloadId); + DebugLogUtils.l("Download requested with id", downloadId); MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId); } return downloadId; @@ -416,7 +434,8 @@ public final class UpdateHandler { /* package */ static void downloadFinished(final Context context, final Intent intent) { // Get and check the ID of the file that was downloaded final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID); - Log.i(TAG, "downloadFinished() : DownloadId = " + fileId); + PrivateLog.log("Download finished with id " + fileId); + DebugLogUtils.l("DownloadFinished with id", fileId); if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); @@ -432,27 +451,31 @@ public final class UpdateHandler { // download, so we are pretty sure it's alive. It's theoretically possible that it's // disabled right inbetween the firing of the intent and the control reaching here. + boolean dictionaryDownloaded = false; + for (final DownloadRecord record : recordList) { // downloadSuccessful is not final because we may still have exceptions from now on boolean downloadSuccessful = false; try { if (downloadInfo.wasSuccessful()) { downloadSuccessful = handleDownloadedFile(context, record, manager, fileId); - Log.i(TAG, "downloadFinished() : Success = " + downloadSuccessful); } } finally { - final String resultMessage = downloadSuccessful ? "Success" : "Failure"; if (record.isMetadata()) { - Log.i(TAG, "downloadFinished() : Metadata " + resultMessage); publishUpdateMetadataCompleted(context, downloadSuccessful); } else { - Log.i(TAG, "downloadFinished() : WordList " + resultMessage); final SQLiteDatabase db = MetadataDbHelper.getDb(context, record.mClientId); publishUpdateWordListCompleted(context, downloadSuccessful, fileId, db, record.mAttributes, record.mClientId); + dictionaryDownloaded = true; } } } + + if (dictionaryDownloaded) { + // Disable the force download after downloading the dictionaries. + CommonPreferences.setForceDownloadDict(context, false); + } // Now that we're done using it, we can remove this download from DLManager manager.remove(fileId); } @@ -569,8 +592,6 @@ public final class UpdateHandler { * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data. */ private static void signalNewDictionaryState(final Context context) { - // TODO: Also provide the locale of the updated dictionary so that the LatinIme - // does not have to reset if it is a different locale. final Intent newDictBroadcast = new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); context.sendBroadcast(newDictBroadcast); @@ -585,7 +606,7 @@ public final class UpdateHandler { * @throws BadFormatException if the metadata is not in a known format. * @throws IOException if the downloaded file can't be read from the disk */ - public static void handleMetadata(final Context context, final InputStream stream, + private static void handleMetadata(final Context context, final InputStream stream, final String clientId) throws IOException, BadFormatException { DebugLogUtils.l("Entering handleMetadata"); final List<WordListMetadata> newMetadata; @@ -809,7 +830,8 @@ public final class UpdateHandler { actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); if (status == MetadataDbHelper.STATUS_INSTALLED || status == MetadataDbHelper.STATUS_DISABLED) { - actions.add(new ActionBatch.StartDownloadAction(clientId, newInfo)); + actions.add(new ActionBatch.StartDownloadAction( + clientId, newInfo, CommonPreferences.isForceDownloadDict(context))); } else { // Pass true to ForgetAction: this is indeed an update to a non-installed // word list, so activate status == AVAILABLE check @@ -907,9 +929,7 @@ public final class UpdateHandler { // list because it may only install the latest version we know about for this specific // word list ID / client ID combination. public static void installIfNeverRequested(final Context context, final String clientId, - final String wordlistId) { - Log.i(TAG, "installIfNeverRequested() : ClientId = " + clientId - + " : WordListId = " + wordlistId); + final String wordlistId, final boolean mayPrompt) { final String[] idArray = wordlistId.split(DictionaryProvider.ID_CATEGORY_SEPARATOR); // If we have a new-format dictionary id (category:manual_id), then use the // specified category. Otherwise, it is a main dictionary, so force the @@ -942,6 +962,17 @@ public final class UpdateHandler { return; } + if (mayPrompt + && DOWNLOAD_OVER_METERED_SETTING_UNKNOWN + == getDownloadOverMeteredSetting(context)) { + final ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (ConnectivityManagerCompatUtils.isActiveNetworkMetered(cm)) { + showDictionaryAvailableNotification(context, clientId, installCandidate); + return; + } + } + // We decided against prompting the user for a decision. This may be because we were // explicitly asked not to, or because we are currently on wi-fi anyway, or because we // already know the answer to the question. We'll enqueue a request ; StartDownloadAction @@ -953,18 +984,21 @@ public final class UpdateHandler { // change the shared preferences. So there is no way for a word list that has been // auto-installed once to get auto-installed again, and that's what we want. final ActionBatch actions = new ActionBatch(); - WordListMetadata metadata = WordListMetadata.createFromContentValues(installCandidate); - actions.add(new ActionBatch.StartDownloadAction(clientId, metadata)); + actions.add(new ActionBatch.StartDownloadAction( + clientId, + WordListMetadata.createFromContentValues(installCandidate), + CommonPreferences.isForceDownloadDict(context))); final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); // We are in a content provider: we can't do any UI at all. We have to defer the displaying // itself to the service. Also, we only display this when the user does not have a - // dictionary for this language already. - final Intent intent = new Intent(); - intent.setClass(context, DictionaryService.class); - intent.setAction(DictionaryService.SHOW_DOWNLOAD_TOAST_INTENT_ACTION); - intent.putExtra(DictionaryService.LOCALE_INTENT_ARGUMENT, localeString); - context.startService(intent); - Log.i(TAG, "installIfNeverRequested() : StartDownloadAction for " + metadata); + // dictionary for this language already: we know that from the mayPrompt argument. + if (mayPrompt) { + final Intent intent = new Intent(); + intent.setClass(context, DictionaryService.class); + intent.setAction(DictionaryService.SHOW_DOWNLOAD_TOAST_INTENT_ACTION); + intent.putExtra(DictionaryService.LOCALE_INTENT_ARGUMENT, localeString); + context.startService(intent); + } actions.execute(context, new LogProblemReporter(TAG)); } @@ -999,7 +1033,9 @@ public final class UpdateHandler { || MetadataDbHelper.STATUS_DELETING == status) { actions.add(new ActionBatch.EnableAction(clientId, wordListMetaData)); } else if (MetadataDbHelper.STATUS_AVAILABLE == status) { - actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData)); + boolean forceDownloadDict = CommonPreferences.isForceDownloadDict(context); + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData, + forceDownloadDict || allowDownloadOnMeteredData)); } else { Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status); } @@ -1114,7 +1150,8 @@ public final class UpdateHandler { } final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData)); + actions.add(new ActionBatch.StartDownloadAction( + clientId, wordListMetaData, CommonPreferences.isForceDownloadDict(context))); actions.execute(context, new LogProblemReporter(TAG)); } else { if (DEBUG) { diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index d25c1d373..bc62f3ae3 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -29,8 +29,6 @@ import android.util.Log; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.dictionarypack.MD5Calculator; -import com.android.inputmethod.dictionarypack.UpdateHandler; -import com.android.inputmethod.latin.common.FileUtils; import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils.DictionaryInfo; @@ -222,11 +220,11 @@ public final class BinaryDictionaryFileDumper { } /** - * Stages a word list the id of which is passed as an argument. This will write the file + * Caches a word list the id of which is passed as an argument. This will write the file * to the cache file name designated by its id and locale, overwriting it if already present * and creating it (and its containing directory) if necessary. */ - private static void installWordListToStaging(final String wordlistId, final String locale, + private static void cacheWordList(final String wordlistId, final String locale, final String rawChecksum, final ContentProviderClient providerClient, final Context context) { final int COMPRESSED_CRYPTED_COMPRESSED = 0; @@ -248,7 +246,7 @@ public final class BinaryDictionaryFileDumper { return; } final String finalFileName = - DictionaryInfoUtils.getStagingFileName(wordlistId, locale, context); + DictionaryInfoUtils.getCacheFileName(wordlistId, locale, context); String tempFileName; try { tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context); @@ -322,21 +320,23 @@ public final class BinaryDictionaryFileDumper { } } - // move the output file to the final staging file. final File finalFile = new File(finalFileName); - FileUtils.renameTo(outputFile, finalFile); - + finalFile.delete(); + if (!outputFile.renameTo(finalFile)) { + throw new IOException("Can't move the file to its final name"); + } wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, QUERY_PARAMETER_SUCCESS); if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { Log.e(TAG, "Could not have the dictionary pack delete a word list"); } - Log.d(TAG, "Successfully copied file for wordlist ID " + wordlistId); + BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile); + Log.e(TAG, "Successfully copied file for wordlist ID " + wordlistId); // Success! Close files (through the finally{} clause) and return. return; } catch (Exception e) { if (DEBUG) { - Log.e(TAG, "Can't open word list in mode " + mode, e); + Log.i(TAG, "Can't open word list in mode " + mode, e); } if (null != outputFile) { // This may or may not fail. The file may not have been created if the @@ -403,7 +403,7 @@ public final class BinaryDictionaryFileDumper { } /** - * Queries a content provider for word list data for some locale and stage the returned files + * Queries a content provider for word list data for some locale and cache the returned files * * This will query a content provider for word list data for a given locale, and copy the * files locally so that they can be mmap'ed. This may overwrite previously cached word lists @@ -411,7 +411,7 @@ public final class BinaryDictionaryFileDumper { * @throw FileNotFoundException if the provider returns non-existent data. * @throw IOException if the provider-returned data could not be read. */ - public static void installDictToStagingFromContentProvider(final Locale locale, + public static void cacheWordListsFromContentProvider(final Locale locale, final Context context, final boolean hasDefaultWordList) { final ContentProviderClient providerClient; try { @@ -429,8 +429,7 @@ public final class BinaryDictionaryFileDumper { final List<WordListInfo> idList = getWordListWordListInfos(locale, context, hasDefaultWordList); for (WordListInfo id : idList) { - installWordListToStaging(id.mId, id.mLocale, id.mRawChecksum, providerClient, - context); + cacheWordList(id.mId, id.mLocale, id.mRawChecksum, providerClient, context); } } finally { providerClient.release(); @@ -438,18 +437,6 @@ public final class BinaryDictionaryFileDumper { } /** - * Downloads the dictionary if it was never requested/used. - * - * @param locale locale to download - * @param context the context for resources and providers. - * @param hasDefaultWordList whether the default wordlist exists in the resources. - */ - public static void downloadDictIfNeverRequested(final Locale locale, - final Context context, final boolean hasDefaultWordList) { - getWordListWordListInfos(locale, context, hasDefaultWordList); - } - - /** * Copies the data in an input stream to a target file if the magic number matches. * * If the magic number does not match the expected value, this method throws an @@ -488,8 +475,6 @@ public final class BinaryDictionaryFileDumper { private static void reinitializeClientRecordInDictionaryContentProvider(final Context context, final ContentProviderClient client, final String clientId) throws RemoteException { final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context); - Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : MetadataFileUri = " - + metadataFileUri); final String metadataAdditionalId = MetadataFileUriGetter.getMetadataAdditionalId(context); // Tell the content provider to reset all information about this client id final Uri metadataContentUri = getProviderUriBuilder(clientId) @@ -514,34 +499,9 @@ public final class BinaryDictionaryFileDumper { final int length = dictionaryList.size(); for (int i = 0; i < length; ++i) { final DictionaryInfo info = dictionaryList.get(i); - Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : Insert " + info); client.insert(Uri.withAppendedPath(dictionaryContentUriBase, info.mId), info.toContentValues()); } - - // Read from metadata file in resources to get the baseline dictionary info. - // This ensures we start with a sane list of available dictionaries. - final int metadataResourceId = context.getResources().getIdentifier("metadata", - "raw", DictionaryInfoUtils.RESOURCE_PACKAGE_NAME); - if (metadataResourceId == 0) { - Log.w(TAG, "Missing metadata.json resource"); - return; - } - InputStream inputStream = null; - try { - inputStream = context.getResources().openRawResource(metadataResourceId); - UpdateHandler.handleMetadata(context, inputStream, clientId); - } catch (Exception e) { - Log.w(TAG, "Failed to read metadata.json from resources", e); - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - Log.w(TAG, "Failed to close metadata.json", e); - } - } - } } /** diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 60016371b..5f2a112ba 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -195,6 +195,39 @@ final public class BinaryDictionaryGetter { return result; } + /** + * Remove all files with the passed id, except the passed file. + * + * If a dictionary with a given ID has a metadata change that causes it to change + * path, we need to remove the old version. The only way to do this is to check all + * installed files for a matching ID in a different directory. + */ + public static void removeFilesWithIdExcept(final Context context, final String id, + final File fileToKeep) { + try { + final File canonicalFileToKeep = fileToKeep.getCanonicalFile(); + final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context); + if (null == directoryList) return; + for (File directory : directoryList) { + // There is one directory per locale. See #getCachedDirectoryList + if (!directory.isDirectory()) continue; + final File[] wordLists = directory.listFiles(); + if (null == wordLists) continue; + for (File wordList : wordLists) { + final String fileId = + DictionaryInfoUtils.getWordListIdFromFileName(wordList.getName()); + if (fileId.equals(id)) { + if (!canonicalFileToKeep.equals(wordList.getCanonicalFile())) { + wordList.delete(); + } + } + } + } + } catch (java.io.IOException e) { + Log.e(TAG, "IOException trying to cleanup files", e); + } + } + // ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since // those do not include whitelist entries, the new code with an old version of the dictionary // would lose whitelist functionality. @@ -241,18 +274,12 @@ final public class BinaryDictionaryGetter { */ public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale, final Context context, boolean notifyDictionaryPackForUpdates) { - if (notifyDictionaryPackForUpdates) { - final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( - context, locale); - // It makes sure that the first time keyboard comes up and the dictionaries are reset, - // the DB is populated with the appropriate values for each locale. Helps in downloading - // the dictionaries when the user enables and switches new languages before the - // DictionaryService runs. - BinaryDictionaryFileDumper.downloadDictIfNeverRequested( - locale, context, hasDefaultWordList); - // Move a staging files to the cache ddirectories if any. - DictionaryInfoUtils.moveStagingFilesIfExists(context); + final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( + context, locale); + if (notifyDictionaryPackForUpdates) { + BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context, + hasDefaultWordList); } final File[] cachedWordLists = getCachedWordLists(locale.toString(), context); final String mainDictId = DictionaryInfoUtils.getMainDictId(locale); diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index 02015da09..ff798abd6 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -17,7 +17,6 @@ package com.android.inputmethod.latin; import android.content.Context; -import android.util.LruCache; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.Keyboard; @@ -56,18 +55,6 @@ public interface DictionaryFacilitator { Dictionary.TYPE_USER}; /** - * The facilitator will put words into the cache whenever it decodes them. - * @param cache - */ - void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache); - - /** - * The facilitator will get words from the cache whenever it needs to check their spelling. - * @param cache - */ - void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache); - - /** * Returns whether this facilitator is exactly for this locale. * * @param locale the locale to test against @@ -101,16 +88,12 @@ public interface DictionaryFacilitator { * * WARNING: The service methods that call start/finish are very spammy. */ - void onFinishInput(Context context); + void onFinishInput(); boolean isActive(); Locale getLocale(); - boolean usesContacts(); - - String getAccount(); - void resetDictionaries( final Context context, final Locale newLocale, @@ -166,7 +149,7 @@ public interface DictionaryFacilitator { boolean isValidSuggestionWord(final String word); - boolean clearUserHistoryDictionary(final Context context); + void clearUserHistoryDictionary(final Context context); String dump(final Context context); diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java index c7115c9d9..7233d27ab 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -19,7 +19,6 @@ package com.android.inputmethod.latin; import android.content.Context; import android.text.TextUtils; import android.util.Log; -import android.util.LruCache; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.Keyboard; @@ -27,7 +26,6 @@ import com.android.inputmethod.latin.NgramContext.WordInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.common.ComposedData; import com.android.inputmethod.latin.common.Constants; -import com.android.inputmethod.latin.common.StringUtils; import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.utils.ExecutorUtils; @@ -84,19 +82,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = new Class[] { Context.class, Locale.class, File.class, String.class, String.class }; - private LruCache<String, Boolean> mValidSpellingWordReadCache; - private LruCache<String, Boolean> mValidSpellingWordWriteCache; - - @Override - public void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache) { - mValidSpellingWordReadCache = cache; - } - - @Override - public void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache) { - mValidSpellingWordWriteCache = cache; - } - @Override public boolean isForLocale(final Locale locale) { return locale != null && locale.equals(mDictionaryGroup.mLocale); @@ -222,7 +207,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } @Override - public void onFinishInput(Context context) { + public void onFinishInput() { } @Override @@ -235,16 +220,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { return mDictionaryGroup.mLocale; } - @Override - public boolean usesContacts() { - return mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTACTS) != null; - } - - @Override - public String getAccount() { - return null; - } - @Nullable private static ExpandableBinaryDictionary getSubDict(final String dictType, final Context context, final Locale locale, final File dictFile, @@ -366,10 +341,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { dictionarySetToCleanup.closeDict(dictType); } } - - if (mValidSpellingWordWriteCache != null) { - mValidSpellingWordWriteCache.evictAll(); - } } private void asyncReloadUninitializedMainDictionaries(final Context context, @@ -493,10 +464,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, final boolean blockPotentiallyOffensive) { - // Update the spelling cache before learning. Words that are not yet added to user history - // and appear in no other language model are not considered valid. - putWordIntoValidSpellingWordCache("addToUserHistory", suggestion); - final String[] words = suggestion.split(Constants.WORD_SEPARATOR); NgramContext ngramContextForCurrentWord = ngramContext; for (int i = 0; i < words.length; i++) { @@ -510,29 +477,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } } - private void putWordIntoValidSpellingWordCache( - @Nonnull final String caller, - @Nonnull final String originalWord) { - if (mValidSpellingWordWriteCache == null) { - return; - } - - final String lowerCaseWord = originalWord.toLowerCase(getLocale()); - final boolean lowerCaseValid = isValidSpellingWord(lowerCaseWord); - mValidSpellingWordWriteCache.put(lowerCaseWord, lowerCaseValid); - - final String capitalWord = - StringUtils.capitalizeFirstAndDowncaseRest(originalWord, getLocale()); - final boolean capitalValid; - if (lowerCaseValid) { - // The lower case form of the word is valid, so the upper case must be valid. - capitalValid = true; - } else { - capitalValid = isValidSpellingWord(capitalWord); - } - mValidSpellingWordWriteCache.put(capitalWord, capitalValid); - } - private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { @@ -599,10 +543,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { if (eventType != Constants.EVENT_BACKSPACE) { removeWord(Dictionary.TYPE_USER_HISTORY, word); } - - // Update the spelling cache after unlearning. Words that are removed from user history - // and appear in no other language model are not considered valid. - putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.toLowerCase()); } // TODO: Revise the way to fusion suggestion results. @@ -637,13 +577,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } public boolean isValidSpellingWord(final String word) { - if (mValidSpellingWordReadCache != null) { - final Boolean cachedValue = mValidSpellingWordReadCache.get(word); - if (cachedValue != null) { - return cachedValue; - } - } - return isValidWord(word, ALL_DICTIONARY_TYPES); } @@ -687,18 +620,16 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { return maxFreq; } - private boolean clearSubDictionary(final String dictName) { + private void clearSubDictionary(final String dictName) { final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); - if (dictionary == null) { - return false; + if (dictionary != null) { + dictionary.clear(); } - dictionary.clear(); - return true; } @Override - public boolean clearUserHistoryDictionary(final Context context) { - return clearSubDictionary(Dictionary.TYPE_USER_HISTORY); + public void clearUserHistoryDictionary(final Context context) { + clearSubDictionary(Dictionary.TYPE_USER_HISTORY); } @Override diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 089670ebf..330be377b 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -972,7 +972,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen void onFinishInputInternal() { super.onFinishInput(); - mDictionaryFacilitator.onFinishInput(this); + mDictionaryFacilitator.onFinishInput(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index a10f2bdb0..a123d282b 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -16,10 +16,11 @@ package com.android.inputmethod.latin; +import static com.android.inputmethod.latin.define.DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH; + import android.inputmethodservice.InputMethodService; import android.os.Build; import android.os.Bundle; -import android.os.SystemClock; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.CharacterStyle; @@ -36,6 +37,7 @@ import com.android.inputmethod.compat.InputConnectionCompatUtils; import com.android.inputmethod.latin.common.Constants; import com.android.inputmethod.latin.common.UnicodeSurrogate; import com.android.inputmethod.latin.common.StringUtils; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.CapsModeUtils; @@ -43,11 +45,8 @@ import com.android.inputmethod.latin.utils.DebugLogUtils; import com.android.inputmethod.latin.utils.NgramContextUtils; import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.SpannableStringUtils; -import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.TextRange; -import java.util.concurrent.TimeUnit; - import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -60,42 +59,17 @@ import javax.annotation.Nullable; * for example. */ public final class RichInputConnection implements PrivateCommandPerformer { - private static final String TAG = "RichInputConnection"; + private static final String TAG = RichInputConnection.class.getSimpleName(); private static final boolean DBG = false; private static final boolean DEBUG_PREVIOUS_TEXT = false; private static final boolean DEBUG_BATCH_NESTING = false; - private static final int NUM_CHARS_TO_GET_BEFORE_CURSOR = 40; - private static final int NUM_CHARS_TO_GET_AFTER_CURSOR = 40; + // Provision for realistic N-grams like "Hello, how are you?" and "I'm running 5 late". + // Technically, this will not handle 5-grams composed of long words, but in practice, + // our language models don't include that much data. + private static final int LOOKBACK_CHARACTER_NUM = 80; private static final int INVALID_CURSOR_POSITION = -1; /** - * The amount of time a {@link #reloadTextCache} call needs to take for the keyboard to enter - * the {@link #hasSlowInputConnection} state. - */ - private static final long SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS = 1000; - /** - * The amount of time a {@link #getTextBeforeCursor} or {@link #getTextAfterCursor} call needs - * to take for the keyboard to enter the {@link #hasSlowInputConnection} state. - */ - private static final long SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS = 200; - - private static final int OPERATION_GET_TEXT_BEFORE_CURSOR = 0; - private static final int OPERATION_GET_TEXT_AFTER_CURSOR = 1; - private static final int OPERATION_GET_WORD_RANGE_AT_CURSOR = 2; - private static final int OPERATION_RELOAD_TEXT_CACHE = 3; - private static final String[] OPERATION_NAMES = new String[] { - "GET_TEXT_BEFORE_CURSOR", - "GET_TEXT_AFTER_CURSOR", - "GET_WORD_RANGE_AT_CURSOR", - "RELOAD_TEXT_CACHE"}; - - /** - * The amount of time the keyboard will persist in the {@link #hasSlowInputConnection} state - * after observing a slow InputConnection event. - */ - private static final long SLOW_INPUTCONNECTION_PERSIST_MS = TimeUnit.MINUTES.toMillis(10); - - /** * This variable contains an expected value for the selection start position. This is where the * cursor or selection start may end up after all the keyboard-triggered updates have passed. We * keep this to compare it to the actual selection start to guess whether the move was caused by @@ -111,7 +85,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points /** * This contains the committed text immediately preceding the cursor and the composing - * text, if any. It is refreshed when the cursor moves by calling upon the TextView. + * text if any. It is refreshed when the cursor moves by calling upon the TextView. */ private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); /** @@ -126,13 +100,8 @@ public final class RichInputConnection implements PrivateCommandPerformer { private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder(); private final InputMethodService mParent; - private InputConnection mIC; - private int mNestLevel; - - /** - * The timestamp of the last slow InputConnection operation - */ - private long mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS; + InputConnection mIC; + int mNestLevel; public RichInputConnection(final InputMethodService parent) { mParent = parent; @@ -144,19 +113,6 @@ public final class RichInputConnection implements PrivateCommandPerformer { return mIC != null; } - /** - * Returns whether or not the underlying InputConnection is slow. When true, we want to avoid - * calling InputConnection methods that trigger an IPC round-trip (e.g., getTextAfterCursor). - */ - public boolean hasSlowInputConnection() { - return (SystemClock.uptimeMillis() - mLastSlowInputConnectionTime) - <= SLOW_INPUTCONNECTION_PERSIST_MS; - } - - public void onStartInput() { - mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS; - } - private void checkConsistencyForDebug() { final ExtractedTextRequest r = new ExtractedTextRequest(); r.hintMaxChars = 0; @@ -255,11 +211,9 @@ public final class RichInputConnection implements PrivateCommandPerformer { mIC = mParent.getCurrentInputConnection(); // Call upon the inputconnection directly since our own method is using the cache, and // we want to refresh it. - final CharSequence textBeforeCursor = getTextBeforeCursorAndDetectLaggyConnection( - OPERATION_RELOAD_TEXT_CACHE, - SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS, - Constants.EDITOR_CONTENTS_CACHE_SIZE, - 0 /* flags */); + final CharSequence textBeforeCursor = isConnected() + ? mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0) + : null; if (null == textBeforeCursor) { // For some reason the app thinks we are not connected to it. This looks like a // framework bug... Fall back to ground state and return false. @@ -423,54 +377,16 @@ public final class RichInputConnection implements PrivateCommandPerformer { } return s; } - return getTextBeforeCursorAndDetectLaggyConnection( - OPERATION_GET_TEXT_BEFORE_CURSOR, - SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, - n, flags); - } - - private CharSequence getTextBeforeCursorAndDetectLaggyConnection( - final int operation, final long timeout, final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - if (!isConnected()) { - return null; - } - final long startTime = SystemClock.uptimeMillis(); - final CharSequence result = mIC.getTextBeforeCursor(n, flags); - detectLaggyConnection(operation, timeout, startTime); - return result; + return isConnected() ? mIC.getTextBeforeCursor(n, flags) : null; } public CharSequence getTextAfterCursor(final int n, final int flags) { - return getTextAfterCursorAndDetectLaggyConnection( - OPERATION_GET_TEXT_AFTER_CURSOR, - SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, - n, flags); - } - - private CharSequence getTextAfterCursorAndDetectLaggyConnection( - final int operation, final long timeout, final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - if (!isConnected()) { - return null; - } - final long startTime = SystemClock.uptimeMillis(); - final CharSequence result = mIC.getTextAfterCursor(n, flags); - detectLaggyConnection(operation, timeout, startTime); - return result; - } - - private void detectLaggyConnection(final int operation, final long timeout, final long startTime) { - final long duration = SystemClock.uptimeMillis() - startTime; - if (duration >= timeout) { - final String operationName = OPERATION_NAMES[operation]; - Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms."); - StatsUtils.onInputConnectionLaggy(operation, duration); - mLastSlowInputConnectionTime = SystemClock.uptimeMillis(); - } + return isConnected() ? mIC.getTextAfterCursor(n, flags) : null; } - public void deleteTextBeforeCursor(final int beforeLength) { + public void deleteSurroundingText(final int beforeLength, final int afterLength) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); // TODO: the following is incorrect if the cursor is not immediately after the composition. // Right now we never come here in this case because we reset the composing state before we @@ -495,7 +411,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { mExpectedSelStart = 0; } if (isConnected()) { - mIC.deleteSurroundingText(beforeLength, 0); + mIC.deleteSurroundingText(beforeLength, afterLength); } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } @@ -660,9 +576,9 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (!isConnected()) { return NgramContext.EMPTY_PREV_WORDS_INFO; } - final CharSequence prev = getTextBeforeCursor(NUM_CHARS_TO_GET_BEFORE_CURSOR, 0); + final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); if (DEBUG_PREVIOUS_TEXT && null != prev) { - final int checkLength = NUM_CHARS_TO_GET_BEFORE_CURSOR - 1; + final int checkLength = LOOKBACK_CHARACTER_NUM - 1; final String reference = prev.length() <= checkLength ? prev.toString() : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); // TODO: right now the following works because mComposingText holds the part of the @@ -705,15 +621,9 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (!isConnected()) { return null; } - final CharSequence before = getTextBeforeCursorAndDetectLaggyConnection( - OPERATION_GET_WORD_RANGE_AT_CURSOR, - SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, - NUM_CHARS_TO_GET_BEFORE_CURSOR, + final CharSequence before = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, InputConnection.GET_TEXT_WITH_STYLES); - final CharSequence after = getTextAfterCursorAndDetectLaggyConnection( - OPERATION_GET_WORD_RANGE_AT_CURSOR, - SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, - NUM_CHARS_TO_GET_AFTER_CURSOR, + final CharSequence after = mIC.getTextAfterCursor(LOOKBACK_CHARACTER_NUM, InputConnection.GET_TEXT_WITH_STYLES); if (before == null || after == null) { return null; @@ -756,9 +666,8 @@ public final class RichInputConnection implements PrivateCommandPerformer { hasUrlSpans); } - public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations, - boolean checkTextAfter) { - if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) { + public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { + if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { // If what's after the cursor is a word character, then we're touching a word. return true; } @@ -795,7 +704,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (DEBUG_BATCH_NESTING) checkBatchEdit(); final int codePointBeforeCursor = getCodePointBeforeCursor(); if (Constants.CODE_SPACE == codePointBeforeCursor) { - deleteTextBeforeCursor(1); + deleteSurroundingText(1, 0); } } @@ -821,7 +730,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { } // Double-space results in ". ". A backspace to cancel this should result in a single // space in the text field, so we replace ". " with a single space. - deleteTextBeforeCursor(2); + deleteSurroundingText(2, 0); final String singleSpace = " "; commitText(singleSpace, 1); return true; @@ -843,7 +752,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { + "find a space just before the cursor."); return false; } - deleteTextBeforeCursor(2); + deleteSurroundingText(2, 0); final String text = " " + textBeforeCursor.subSequence(0, 1); commitText(text, 1); return true; diff --git a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java index 90221512f..0d081e0d2 100644 --- a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java @@ -16,7 +16,6 @@ package com.android.inputmethod.latin; -import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -24,15 +23,14 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.database.Cursor; import android.os.Process; import android.preference.PreferenceManager; import android.util.Log; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.dictionarypack.CommonPreferences; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; -import com.android.inputmethod.dictionarypack.DownloadManagerWrapper; import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.setup.SetupActivity; @@ -77,12 +75,7 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(); richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); toggleAppIcon(context); - - // Remove all the previously scheduled downloads. This will also makes sure - // that any erroneously stuck downloads will get cleared. (b/21797386) - removeOldDownloads(context); - // b/21797386 - // downloadLatestDictionaries(context); + downloadLatestDictionaries(context); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { Log.i(TAG, "Boot has been completed"); toggleAppIcon(context); @@ -110,39 +103,13 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { } } - private void removeOldDownloads(Context context) { - try { - Log.i(TAG, "Removing the old downloads in progress of the previous keyboard version."); - final DownloadManagerWrapper downloadManagerWrapper = new DownloadManagerWrapper( - context); - final DownloadManager.Query q = new DownloadManager.Query(); - // Query all the download statuses except the succeeded ones. - q.setFilterByStatus(DownloadManager.STATUS_FAILED - | DownloadManager.STATUS_PAUSED - | DownloadManager.STATUS_PENDING - | DownloadManager.STATUS_RUNNING); - final Cursor c = downloadManagerWrapper.query(q); - if (c != null) { - for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) { - final long downloadId = c - .getLong(c.getColumnIndex(DownloadManager.COLUMN_ID)); - downloadManagerWrapper.remove(downloadId); - Log.i(TAG, "Removed the download with Id: " + downloadId); - } - c.close(); - } - } catch (Exception e) { - Log.e(TAG, "Exception while removing old downloads."); - } - } - private void downloadLatestDictionaries(Context context) { final Intent updateIntent = new Intent( DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION); context.sendBroadcast(updateIntent); } - public static void toggleAppIcon(final Context context) { + private static void toggleAppIcon(final Context context) { final int appInfoFlags = context.getApplicationInfo().flags; final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0; if (Log.isLoggable(TAG, Log.INFO)) { diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 1dd5850f8..f7dbc0a4d 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -139,7 +139,6 @@ public final class InputLogic { public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; mWordBeingCorrectedByCursor = null; - mConnection.onStartInput(); if (!mWordComposer.getTypedWord().isEmpty()) { // For messaging apps that offer send button, the IME does not get the opportunity // to capture the last word. This block should capture those uncommitted words. @@ -399,8 +398,9 @@ public final class InputLogic { if (!TextUtils.isEmpty(mWordBeingCorrectedByCursor)) { final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( System.currentTimeMillis()); - performAdditionToUserHistoryDictionary(settingsValues, mWordBeingCorrectedByCursor, - NgramContext.EMPTY_PREV_WORDS_INFO); + mDictionaryFacilitator.addToUserHistory(mWordBeingCorrectedByCursor, false, + NgramContext.EMPTY_PREV_WORDS_INFO, timeStampInSeconds, + settingsValues.mBlockPotentiallyOffensive); } } else { // resetEntireInputState calls resetCachesUponCursorMove, but forcing the @@ -473,7 +473,7 @@ public final class InputLogic { } // Try to record the word being corrected when the user enters a word character or // the backspace key. - if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord() + if (!mWordComposer.isComposingWord() && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) || processedEvent.mKeyCode == Constants.CODE_DELETE)) { mWordBeingCorrectedByCursor = getWordAtCursor( @@ -833,14 +833,8 @@ public final class InputLogic { && settingsValues.needsToLookupSuggestions() && // In languages with spaces, we only start composing a word when we are not already // touching a word. In languages without spaces, the above conditions are sufficient. - // NOTE: If the InputConnection is slow, we skip the text-after-cursor check since it - // can incur a very expensive getTextAfterCursor() lookup, potentially making the - // keyboard UI slow and non-responsive. - // TODO: Cache the text after the cursor so we don't need to go to the InputConnection - // each time. We are already doing this for getTextBeforeCursor(). - (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces - || !mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, - !mConnection.hasSlowInputConnection() /* checkTextAfter */))) { + (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations) + || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) { // Reset entirely the composing state anyway, then start composing a new word unless // the character is a word connector. The idea here is, word connectors are not // separators and they should be treated as normal characters, except in the first @@ -1060,7 +1054,7 @@ public final class InputLogic { // Cancel multi-character input: remove the text we just entered. // This is triggered on backspace after a key that inputs multiple characters, // like the smiley key or the .com key. - mConnection.deleteTextBeforeCursor(mEnteredText.length()); + mConnection.deleteSurroundingText(mEnteredText.length(), 0); StatsUtils.onDeleteMultiCharInput(mEnteredText.length()); mEnteredText = null; // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. @@ -1105,7 +1099,7 @@ public final class InputLogic { - mConnection.getExpectedSelectionStart(); mConnection.setSelection(mConnection.getExpectedSelectionEnd(), mConnection.getExpectedSelectionEnd()); - mConnection.deleteTextBeforeCursor(numCharsDeleted); + mConnection.deleteSurroundingText(numCharsDeleted, 0); StatsUtils.onBackspaceSelectedText(numCharsDeleted); } else { // There is no selection, just delete one character. @@ -1145,13 +1139,13 @@ public final class InputLogic { // broken apps expect something to happen in this case so that they can // 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.deleteTextBeforeCursor(1); + mConnection.deleteSurroundingText(1, 0); // TODO: Add a new StatsUtils method onBackspaceWhenNoText() return; } final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; - mConnection.deleteTextBeforeCursor(lengthToDelete); + mConnection.deleteSurroundingText(lengthToDelete, 0); int totalDeletedLength = lengthToDelete; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { // If this is an accelerated (i.e., double) deletion, then we need to @@ -1164,7 +1158,7 @@ public final class InputLogic { if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( codePointBeforeCursorToDeleteAgain) ? 2 : 1; - mConnection.deleteTextBeforeCursor(lengthToDeleteAgain); + mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); totalDeletedLength += lengthToDeleteAgain; } } @@ -1176,9 +1170,7 @@ public final class InputLogic { unlearnWordBeingDeleted( inputTransaction.mSettingsValues, currentKeyboardScriptId); } - if (mConnection.hasSlowInputConnection()) { - mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); - } else if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() + if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() && inputTransaction.mSettingsValues.mSpacingAndPunctuations .mCurrentLanguageHasSpaces && !mConnection.isCursorFollowedByWordCharacter( @@ -1205,13 +1197,6 @@ public final class InputLogic { boolean unlearnWordBeingDeleted( final SettingsValues settingsValues, final int currentKeyboardScriptId) { - if (mConnection.hasSlowInputConnection()) { - // TODO: Refactor unlearning so that it does not incur any extra calls - // to the InputConnection. That way it can still be performed on a slow - // InputConnection. - Log.w(TAG, "Skipping unlearning due to slow InputConnection."); - return false; - } // If we just started backspacing to delete a previous word (but have not // entered the composing state yet), unlearn the word. // TODO: Consider tracking whether or not this word was typed by the user. @@ -1257,7 +1242,7 @@ public final class InputLogic { if (Constants.CODE_SPACE != codePointBeforeCursor) { return false; } - mConnection.deleteTextBeforeCursor(1); + mConnection.deleteSurroundingText(1, 0); final String text = event.getTextToCommit() + " "; mConnection.commitText(text, 1); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); @@ -1347,7 +1332,7 @@ public final class InputLogic { Character.codePointAt(lastTwo, length - 3) : lastTwo.charAt(length - 2); if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { cancelDoubleSpacePeriodCountdown(); - mConnection.deleteTextBeforeCursor(1); + mConnection.deleteSurroundingText(1, 0); final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations .mSentenceSeparatorAndSpace; mConnection.commitText(textToInsert, 1); @@ -1415,7 +1400,7 @@ public final class InputLogic { mConnection.finishComposingText(); mRecapitalizeStatus.rotate(); mConnection.setSelection(selectionEnd, selectionEnd); - mConnection.deleteTextBeforeCursor(numCharsSelected); + mConnection.deleteSurroundingText(numCharsSelected, 0); mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(), mRecapitalizeStatus.getNewCursorEnd()); @@ -1427,12 +1412,6 @@ public final class InputLogic { // That's to avoid unintended additions in some sensitive fields, or fields that // expect to receive non-words. if (!settingsValues.mAutoCorrectionEnabledPerUserSettings) return; - if (mConnection.hasSlowInputConnection()) { - // Since we don't unlearn when the user backspaces on a slow InputConnection, - // turn off learning to guard against adding typos that the user later deletes. - Log.w(TAG, "Skipping learning due to slow InputConnection."); - return; - } if (TextUtils.isEmpty(suggestion)) return; final boolean wasAutoCapitalized = @@ -1536,8 +1515,7 @@ public final class InputLogic { return; } final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); - if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, - true /* checkTextAfter */)) { + if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { // Show predictions. mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF); mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION); @@ -1660,7 +1638,7 @@ public final class InputLogic { + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); } } - mConnection.deleteTextBeforeCursor(deleteLength); + mConnection.deleteSurroundingText(deleteLength, 0); if (!TextUtils.isEmpty(committedWord)) { unlearnWord(committedWordString, inputTransaction.mSettingsValues, Constants.EVENT_REVERT); @@ -2158,10 +2136,9 @@ public final class InputLogic { final SuggestedWords suggestedWords = mSuggestedWords; // TODO: Locale should be determined based on context and the text given. final Locale locale = getDictionaryFacilitatorLocale(); - final CharSequence chosenWordWithSuggestions = chosenWord; - // b/21926256 - // SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, - // suggestedWords, locale); + final CharSequence chosenWordWithSuggestions = + SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, + suggestedWords, locale); if (DebugFlags.DEBUG_ENABLED) { long runTimeMillis = System.currentTimeMillis() - startTimeMillis; Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java index a6fb7f1f1..f2e1aed4c 100644 --- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java @@ -26,7 +26,6 @@ import android.preference.Preference; import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.SystemBroadcastReceiver; import com.android.inputmethod.latin.define.ProductionFlags; /** @@ -107,8 +106,6 @@ public final class AdvancedSettingsFragment extends SubScreenFragment { if (key.equals(Settings.PREF_POPUP_ON)) { setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, Settings.readKeyPreviewPopupEnabled(prefs, res)); - } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { - SystemBroadcastReceiver.toggleAppIcon(getActivity()); } updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); refreshEnablingsOfKeypressSoundAndVibrationSettings(); diff --git a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java index d28e703fe..aa73a9a83 100644 --- a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java @@ -16,27 +16,20 @@ package com.android.inputmethod.latin.settings; -import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.os.Build; import android.os.Bundle; import android.preference.Preference; import com.android.inputmethod.dictionarypack.DictionarySettingsActivity; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.userdictionary.UserDictionaryList; -import com.android.inputmethod.latin.userdictionary.UserDictionarySettings; - -import java.util.TreeSet; /** * "Text correction" settings sub screen. * * This settings sub screen handles the following text correction preferences. - * - Personal dictionary * - Add-on dictionaries * - Block offensive words * - Auto-correction @@ -66,39 +59,5 @@ public final class CorrectionSettingsFragment extends SubScreenFragment { if (0 >= number) { removePreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY); } - - final Preference editPersonalDictionary = - findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY); - final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent(); - final ResolveInfo ri = USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS ? null - : pm.resolveActivity( - editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY); - if (ri == null) { - overwriteUserDictionaryPreference(editPersonalDictionary); - } - } - - private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) { - final Activity activity = getActivity(); - final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity); - if (null == localeList) { - // The locale list is null if and only if the user dictionary service is - // not present or disabled. In this case we need to remove the preference. - getPreferenceScreen().removePreference(userDictionaryPreference); - } else if (localeList.size() <= 1) { - userDictionaryPreference.setFragment(UserDictionarySettings.class.getName()); - // If the size of localeList is 0, we don't set the locale parameter in the - // extras. This will be interpreted by the UserDictionarySettings class as - // meaning "the current locale". - // Note that with the current code for UserDictionaryList#getUserDictionaryLocalesSet() - // the locale list always has at least one element, since it always includes the current - // locale explicitly. @see UserDictionaryList.getUserDictionaryLocalesSet(). - if (localeList.size() == 1) { - final String locale = (String)localeList.toArray()[0]; - userDictionaryPreference.getExtras().putString("locale", locale); - } - } else { - userDictionaryPreference.setFragment(UserDictionaryList.class.getName()); - } } } diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 940f1bdfc..694f43d3f 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -56,7 +56,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead. public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode"; public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key"; - public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary"; public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key"; // PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE is obsolete. Use PREF_AUTO_CORRECTION instead. public static final String PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE = diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java index c7622e7a1..2c690aea7 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java @@ -84,7 +84,8 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck if (TextUtils.isEmpty(splitText)) { continue; } - if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString()) == null) { + if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString(), ngramContext) + == null) { continue; } final int newLength = splitText.length(); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index 9223923a7..1322ce240 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -71,26 +71,30 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } protected static final class SuggestionsCache { + private static final char CHAR_DELIMITER = '\uFFFC'; private static final int MAX_CACHE_SIZE = 50; private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache = new LruCache<>(MAX_CACHE_SIZE); - private static String generateKey(final String query) { - return query + ""; + private static String generateKey(final String query, final NgramContext ngramContext) { + if (TextUtils.isEmpty(query) || !ngramContext.isValid()) { + return query; + } + return query + CHAR_DELIMITER + ngramContext; } - public SuggestionsParams getSuggestionsFromCache(final String query) { - return mUnigramSuggestionsInfoCache.get(query); + public SuggestionsParams getSuggestionsFromCache(String query, + final NgramContext ngramContext) { + return mUnigramSuggestionsInfoCache.get(generateKey(query, ngramContext)); } - public void putSuggestionsToCache( - final String query, final String[] suggestions, final int flags) { + public void putSuggestionsToCache(final String query, final NgramContext ngramContext, + final String[] suggestions, final int flags) { if (suggestions == null || TextUtils.isEmpty(query)) { return; } mUnigramSuggestionsInfoCache.put( - generateKey(query), - new SuggestionsParams(suggestions, flags)); + generateKey(query, ngramContext), new SuggestionsParams(suggestions, flags)); } public void clearCache() { @@ -228,7 +232,16 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { AndroidSpellCheckerService.SINGLE_QUOTE). replaceAll("^" + quotesRegexp, ""). replaceAll(quotesRegexp + "$", ""); + final SuggestionsParams cachedSuggestionsParams = + mSuggestionsCache.getSuggestionsFromCache(text, ngramContext); + + if (cachedSuggestionsParams != null) { + Log.d(TAG, "onGetSuggestionsInternal() : Cache hit for [" + text + "]"); + return new SuggestionsInfo( + cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions); + } + // If spell checking is impossible, return early. if (!mService.hasMainDictionaryForLocale(mLocale)) { return AndroidSpellCheckerService.getNotInDictEmptySuggestions( false /* reportAsTypo */); @@ -316,7 +329,8 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() : 0); final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions); - mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags); + mSuggestionsCache.putSuggestionsToCache(text, ngramContext, result.mSuggestions, + flags); return retval; } catch (RuntimeException e) { // Don't kill the keyboard if there is a bug in the spell checker diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index 11cccd5fa..cfa977a46 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -30,7 +30,6 @@ import com.android.inputmethod.latin.AssetFileAddress; import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; -import com.android.inputmethod.latin.common.FileUtils; import com.android.inputmethod.latin.common.LocaleUtils; import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.makedict.DictionaryHeader; @@ -54,7 +53,7 @@ import javax.annotation.Nullable; */ public class DictionaryInfoUtils { private static final String TAG = DictionaryInfoUtils.class.getSimpleName(); - public static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); + private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); private static final String DEFAULT_MAIN_DICT = "main"; private static final String MAIN_DICT_PREFIX = "main_"; private static final String DECODER_DICT_SUFFIX = DecoderSpecificConstants.DECODER_DICT_SUFFIX; @@ -103,13 +102,6 @@ public class DictionaryInfoUtils { values.put(VERSION_COLUMN, mVersion); return values; } - - @Override - public String toString() { - return "DictionaryInfo : Id = '" + mId - + "' : Locale=" + mLocale - + " : Version=" + mVersion; - } } private DictionaryInfoUtils() { @@ -161,13 +153,6 @@ public class DictionaryInfoUtils { } /** - * Helper method to get the top level cache directory. - */ - public static String getWordListStagingDirectory(final Context context) { - return context.getFilesDir() + File.separator + "staging"; - } - - /** * Helper method to get the top level temp directory. */ public static String getWordListTempDirectory(final Context context) { @@ -203,10 +188,6 @@ public class DictionaryInfoUtils { return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles(); } - public static File[] getStagingDirectoryList(final Context context) { - return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles(); - } - @Nullable public static File[] getUnusedDictionaryList(final Context context) { return context.getFilesDir().listFiles(new FilenameFilter() { @@ -240,7 +221,7 @@ public class DictionaryInfoUtils { /** * Find out the cache directory associated with a specific locale. */ - public static String getCacheDirectoryForLocale(final String locale, final Context context) { + private static String getCacheDirectoryForLocale(final String locale, final Context context) { final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale); final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator + relativeDirectoryName; @@ -273,52 +254,6 @@ public class DictionaryInfoUtils { return getCacheDirectoryForLocale(locale, context) + File.separator + fileName; } - public static String getStagingFileName(String id, String locale, Context context) { - final String stagingDirectory = getWordListStagingDirectory(context); - // create the directory if it does not exist. - final File directory = new File(stagingDirectory); - if (!directory.exists()) { - if (!directory.mkdirs()) { - Log.e(TAG, "Could not create the staging directory."); - } - } - // e.g. id="main:en_in", locale ="en_IN" - final String fileName = replaceFileNameDangerousCharacters( - locale + TEMP_DICT_FILE_SUB + id); - return stagingDirectory + File.separator + fileName; - } - - public static void moveStagingFilesIfExists(Context context) { - final File[] stagingFiles = DictionaryInfoUtils.getStagingDirectoryList(context); - if (stagingFiles != null && stagingFiles.length > 0) { - for (final File stagingFile : stagingFiles) { - final String fileName = stagingFile.getName(); - final int index = fileName.indexOf(TEMP_DICT_FILE_SUB); - if (index == -1) { - // This should never happen. - Log.e(TAG, "Staging file does not have ___ substring."); - continue; - } - final String[] localeAndFileId = fileName.split(TEMP_DICT_FILE_SUB); - if (localeAndFileId.length != 2) { - Log.e(TAG, String.format("malformed staging file %s. Deleting.", - stagingFile.getAbsoluteFile())); - stagingFile.delete(); - continue; - } - - final String locale = localeAndFileId[0]; - // already escaped while moving to staging. - final String fileId = localeAndFileId[1]; - final String cacheDirectoryForLocale = getCacheDirectoryForLocale(locale, context); - final String cacheFilename = cacheDirectoryForLocale + File.separator + fileId; - final File cacheFile = new File(cacheFilename); - // move the staging file to cache file. - FileUtils.renameTo(stagingFile, cacheFile); - } - } - } - public static boolean isMainWordListId(final String id) { final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); // An id is supposed to be in format category:locale, so splitting on the separator |