diff options
Diffstat (limited to 'java/src/com/android/inputmethod/dictionarypack')
19 files changed, 603 insertions, 551 deletions
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java index 3d294acd7..ee142d845 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."); @@ -171,6 +172,8 @@ public final class ActionBatch { final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db, mWordList.mId, mWordList.mVersion); + Log.i(TAG, String.format("Starting the dictionary download with version:" + + " %d and Url: %s", mWordList.mVersion, uri)); DebugLogUtils.l("Starting download of", uri, "with id", downloadId); PrivateLog.log("Starting download of " + uri + ", id : " + downloadId); } @@ -325,8 +328,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 +377,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 +420,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/ButtonSwitcher.java b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java index 6d6c8f5c6..0fa72c3fd 100644 --- a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java +++ b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java @@ -122,19 +122,23 @@ public class ButtonSwitcher extends FrameLayout { mDeleteButton.setTranslationX(STATUS_DELETE == status ? 0 : width); } + // The helper method for {@link AnimatorListenerAdapter}. + void animateButtonIfStatusIsEqual(final View newButton, final int newStatus) { + if (newStatus != mStatus) return; + animateButton(newButton, ANIMATION_IN); + } + private void animateButtonPosition(final int oldStatus, final int newStatus) { final View oldButton = getButton(oldStatus); final View newButton = getButton(newStatus); if (null != oldButton && null != newButton) { // Transition between two buttons : animate out, then in - animateButton(oldButton, ANIMATION_OUT).setListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - if (newStatus != mStatus) return; - animateButton(newButton, ANIMATION_IN); - } - }); + animateButton(oldButton, ANIMATION_OUT).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + animateButtonIfStatusIsEqual(newButton, newStatus); + } + }); } else if (null != oldButton) { animateButton(oldButton, ANIMATION_OUT); } else if (null != newButton) { @@ -159,9 +163,8 @@ public class ButtonSwitcher extends FrameLayout { if (ANIMATION_IN == direction) { button.setClickable(true); return button.animate().translationX(0); - } else { - button.setClickable(false); - return button.animate().translationX(outerX - innerX); } + button.setClickable(false); + return button.animate().translationX(outerX - innerX); } } 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/DictionaryDownloadProgressBar.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java index 1d84e5888..759852025 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java @@ -148,7 +148,7 @@ public class DictionaryDownloadProgressBar extends ProgressBar { } } - private class UpdateHelper implements Runnable { + class UpdateHelper implements Runnable { private int mProgress; @Override public void run() { diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java index 8e026171d..836340a75 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java @@ -32,7 +32,7 @@ import java.util.HashMap; * in case some dictionaries appeared, disappeared, changed states etc. */ public class DictionaryListInterfaceState { - private static class State { + static class State { public boolean mOpen = false; public int mStatus = MetadataDbHelper.STATUS_UNKNOWN; } diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java index f5bd84c8c..659fe5c51 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java @@ -31,6 +31,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.common.LocaleUtils; import com.android.inputmethod.latin.utils.DebugLogUtils; import java.io.File; @@ -255,10 +256,9 @@ public final class DictionaryProvider extends ContentProvider { if (null != dictFiles && dictFiles.size() > 0) { PrivateLog.log("Returned " + dictFiles.size() + " files"); return new ResourcePathCursor(dictFiles); - } else { - PrivateLog.log("No dictionary files for this URL"); - return new ResourcePathCursor(Collections.<WordListInfo>emptyList()); } + PrivateLog.log("No dictionary files for this URL"); + return new ResourcePathCursor(Collections.<WordListInfo>emptyList()); // V2_METADATA and V2_DATAFILE are not supported for query() default: return null; @@ -319,14 +319,13 @@ public final class DictionaryProvider extends ContentProvider { final AssetFileDescriptor afd = getContext().getResources().openRawResourceFd( R.raw.empty); return afd; - } else { - final String localFilename = - wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); - final File f = getContext().getFileStreamPath(localFilename); - final ParcelFileDescriptor pfd = - ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); - return new AssetFileDescriptor(pfd, 0, pfd.getStatSize()); } + final String localFilename = + wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); + final File f = getContext().getFileStreamPath(localFilename); + final ParcelFileDescriptor pfd = + ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); + return new AssetFileDescriptor(pfd, 0, pfd.getStatSize()); } catch (FileNotFoundException e) { // No file : fall through and return null } @@ -461,30 +460,32 @@ public final class DictionaryProvider extends ContentProvider { final String wordlistId = uri.getLastPathSegment(); final String clientId = getClientId(uri); final ContentValues wordList = getWordlistMetadataForWordlistId(clientId, wordlistId); - if (null == wordList) return 0; + if (null == wordList) { + return 0; + } final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN); final int version = wordList.getAsInteger(MetadataDbHelper.VERSION_COLUMN); if (MetadataDbHelper.STATUS_DELETING == status) { UpdateHandler.markAsDeleted(getContext(), clientId, wordlistId, version, status); return 1; - } else if (MetadataDbHelper.STATUS_INSTALLED == status) { + } + 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); final File f = getContext().getFileStreamPath(localFilename); // f.delete() returns true if the file was successfully deleted, false otherwise - if (f.delete()) { - return 1; - } else { - return 0; - } - } else { - Log.e(TAG, "Attempt to delete a file whose status is " + status); - return 0; + return f.delete() ? 1 : 0; } + Log.e(TAG, "Attempt to delete a file whose status is " + status); + return 0; } /** diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java index 41916b614..bbdf2a380 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java @@ -22,9 +22,12 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.IBinder; +import android.util.Log; import android.widget.Toast; +import com.android.inputmethod.latin.BinaryDictionaryFileDumper; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.common.LocaleUtils; import java.util.Locale; import java.util.Random; @@ -32,6 +35,8 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; + /** * Service that handles background tasks for the dictionary provider. * @@ -50,6 +55,8 @@ import java.util.concurrent.TimeUnit; * to access, and mark the current state as such. */ public final class DictionaryService extends Service { + private static final String TAG = DictionaryService.class.getSimpleName(); + /** * The package name, to use in the intent actions. */ @@ -76,19 +83,29 @@ public final class DictionaryService extends Service { * How often, in milliseconds, we want to update the metadata. This is a * floor value; actually, it may happen several hours later, or even more. */ - private static final long UPDATE_FREQUENCY = TimeUnit.DAYS.toMillis(4); + private static final long UPDATE_FREQUENCY_MILLIS = TimeUnit.DAYS.toMillis(4); /** * We are waked around midnight, local time. We want to wake between midnight and 6 am, * roughly. So use a random time between 0 and this delay. */ - private static final int MAX_ALARM_DELAY = (int)TimeUnit.HOURS.toMillis(6); + private static final int MAX_ALARM_DELAY_MILLIS = (int)TimeUnit.HOURS.toMillis(6); /** * How long we consider a "very long time". If no update took place in this time, * the content provider will trigger an update in the background. */ - private static final long VERY_LONG_TIME = TimeUnit.DAYS.toMillis(14); + private static final long VERY_LONG_TIME_MILLIS = TimeUnit.DAYS.toMillis(14); + + /** + * After starting a download, how long we wait before considering it may be stuck. After this + * period is elapsed, if the keyboard tries to download again, then we cancel and re-register + * the request; if it's within this time, we just leave it be. + * It's important to note that we do not re-submit the request merely because the time is up. + * This is only to decide whether to cancel the old one and re-requesting when the keyboard + * fires a new request for the same data. + */ + public static final long NO_CANCEL_DOWNLOAD_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(30); /** * An executor that serializes tasks given to it. @@ -145,9 +162,14 @@ public final class DictionaryService extends Service { final int startId) { final DictionaryService self = this; if (SHOW_DOWNLOAD_TOAST_INTENT_ACTION.equals(intent.getAction())) { - // This is a UI action, it can't be run in another thread - showStartDownloadingToast(this, LocaleUtils.constructLocaleFromString( - intent.getStringExtra(LOCALE_INTENT_ARGUMENT))); + final String localeString = intent.getStringExtra(LOCALE_INTENT_ARGUMENT); + if (localeString == null) { + Log.e(TAG, "Received " + intent.getAction() + " without locale; skipped"); + } else { + // This is a UI action, it can't be run in another thread + showStartDownloadingToast( + this, LocaleUtils.constructLocaleFromString(localeString)); + } } else { // If it's a command that does not require UI, arrange for the work to be done on a // separate thread, so that we can return right away. The executor will spawn a thread @@ -169,15 +191,28 @@ public final class DictionaryService extends Service { return Service.START_REDELIVER_INTENT; } - private static void dispatchBroadcast(final Context context, final Intent intent) { + static void dispatchBroadcast(final Context context, final Intent intent) { 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(intent.getAction())) { // Intent to trigger an update now. - UpdateHandler.tryUpdate(context, false); + 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, true); } else { UpdateHandler.downloadFinished(context, intent); } @@ -188,16 +223,16 @@ public final class DictionaryService extends Service { */ private static void checkTimeAndMaybeSetupUpdateAlarm(final Context context) { // Of all clients, if the one that hasn't been updated for the longest - // is still more recent than UPDATE_FREQUENCY, do nothing. - if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY)) return; + // is still more recent than UPDATE_FREQUENCY_MILLIS, do nothing. + if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY_MILLIS)) return; PrivateLog.log("Date changed - registering alarm"); AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - // Best effort to wake between midnight and MAX_ALARM_DELAY in the morning. + // Best effort to wake between midnight and MAX_ALARM_DELAY_MILLIS in the morning. // It doesn't matter too much if this is very inexact. final long now = System.currentTimeMillis(); - final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY); + final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY_MILLIS); final Intent updateIntent = new Intent(DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION); final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, updateIntent, PendingIntent.FLAG_CANCEL_CURRENT); @@ -223,18 +258,19 @@ public final class DictionaryService extends Service { /** * Refreshes data if it hasn't been refreshed in a very long time. * - * This will check the last update time, and if it's been more than VERY_LONG_TIME, + * This will check the last update time, and if it's been more than VERY_LONG_TIME_MILLIS, * update metadata now - and possibly take subsequent update actions. */ public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) { - if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME)) return; - UpdateHandler.tryUpdate(context, false); + if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME_MILLIS)) return; + UpdateHandler.tryUpdate(context, CommonPreferences.isForceDownloadDict(context)); } /** * Shows a toast informing the user that an automatic dictionary download is starting. */ - private static void showStartDownloadingToast(final Context context, final Locale locale) { + private static void showStartDownloadingToast(final Context context, + @Nonnull final Locale locale) { final String toastText = String.format( context.getString(R.string.toast_downloading_suggestions), locale.getDisplayName()); diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java index 4366348d5..284032beb 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java @@ -18,7 +18,9 @@ package com.android.inputmethod.dictionarypack; import com.android.inputmethod.latin.utils.FragmentUtils; +import android.annotation.TargetApi; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceActivity; @@ -44,8 +46,8 @@ public final class DictionarySettingsActivity extends PreferenceActivity { return modIntent; } - // TODO: Uncomment the override annotation once we start using SDK version 19. - // @Override + @TargetApi(Build.VERSION_CODES.KITKAT) + @Override public boolean isValidFragment(String fragmentName) { return FragmentUtils.isValidFragment(fragmentName); } diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java index 11982fa65..88ea4e6c3 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java @@ -16,6 +16,8 @@ package com.android.inputmethod.dictionarypack; +import com.android.inputmethod.latin.common.LocaleUtils; + import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -26,12 +28,12 @@ import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceGroup; import android.text.TextUtils; -import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -105,16 +107,27 @@ public final class DictionarySettingsFragment extends PreferenceFragment @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - final String metadataUri = - MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId); - // We only add the "Refresh" button if we have a non-empty URL to refresh from. If the - // URL is empty, of course we can't refresh so it makes no sense to display this. - if (!TextUtils.isEmpty(metadataUri)) { - mUpdateNowMenu = - menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, R.string.check_for_updates_now); - mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - refreshNetworkState(); - } + new AsyncTask<Void, Void, String>() { + @Override + protected String doInBackground(Void... params) { + return MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId); + } + + @Override + protected void onPostExecute(String metadataUri) { + // We only add the "Refresh" button if we have a non-empty URL to refresh from. If + // the URL is empty, of course we can't refresh so it makes no sense to display + // this. + if (!TextUtils.isEmpty(metadataUri)) { + if (mUpdateNowMenu == null) { + mUpdateNowMenu = menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, + R.string.check_for_updates_now); + mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + } + refreshNetworkState(); + } + } + }.execute(); } @Override @@ -123,18 +136,25 @@ public final class DictionarySettingsFragment extends PreferenceFragment mChangedSettings = false; UpdateHandler.registerUpdateEventListener(this); final Activity activity = getActivity(); - if (!MetadataDbHelper.isClientKnown(activity, mClientId)) { - Log.i(TAG, "Unknown dictionary pack client: " + mClientId + ". Requesting info."); - final Intent unknownClientBroadcast = - new Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT); - unknownClientBroadcast.putExtra( - DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId); - activity.sendBroadcast(unknownClientBroadcast); - } final IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); getActivity().registerReceiver(mConnectivityChangedReceiver, filter); refreshNetworkState(); + + new Thread("onResume") { + @Override + public void run() { + if (!MetadataDbHelper.isClientKnown(activity, mClientId)) { + Log.i(TAG, "Unknown dictionary pack client: " + mClientId + + ". Requesting info."); + final Intent unknownClientBroadcast = + new Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT); + unknownClientBroadcast.putExtra( + DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId); + activity.sendBroadcast(unknownClientBroadcast); + } + } + }.start(); } @Override @@ -203,25 +223,19 @@ public final class DictionarySettingsFragment extends PreferenceFragment @Override public void updateCycleCompleted() {} - private void refreshNetworkState() { + void refreshNetworkState() { NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); boolean isConnected = null == info ? false : info.isConnected(); if (null != mUpdateNowMenu) mUpdateNowMenu.setEnabled(isConnected); } - private void refreshInterface() { + void refreshInterface() { final Activity activity = getActivity(); if (null == activity) return; - final long lastUpdateDate = - MetadataDbHelper.getLastUpdateDateForClient(getActivity(), mClientId); final PreferenceGroup prefScreen = getPreferenceScreen(); final Collection<? extends Preference> prefList = createInstalledDictSettingsCollection(mClientId); - final String updateNowSummary = getString(R.string.last_update) + " " - + DateUtils.formatDateTime(activity, lastUpdateDate, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); - activity.runOnUiThread(new Runnable() { @Override public void run() { @@ -239,14 +253,14 @@ public final class DictionarySettingsFragment extends PreferenceFragment }); } - private Preference createErrorMessage(final Activity activity, final int messageResource) { + private static Preference createErrorMessage(final Activity activity, final int messageResource) { final Preference message = new Preference(activity); message.setTitle(messageResource); message.setEnabled(false); return message; } - private void removeAnyDictSettings(final PreferenceGroup prefGroup) { + static void removeAnyDictSettings(final PreferenceGroup prefGroup) { for (int i = prefGroup.getPreferenceCount() - 1; i >= 0; --i) { prefGroup.removePreference(prefGroup.getPreference(i)); } @@ -276,7 +290,7 @@ public final class DictionarySettingsFragment extends PreferenceFragment .appendQueryParameter(DictionaryProvider.QUERY_PARAMETER_PROTOCOL_VERSION, "2") .build(); final Activity activity = getActivity(); - final Cursor cursor = null == activity ? null + final Cursor cursor = (null == activity) ? null : activity.getContentResolver().query(contentUri, null, null, null, null); if (null == cursor) { @@ -289,61 +303,57 @@ public final class DictionarySettingsFragment extends PreferenceFragment final ArrayList<Preference> result = new ArrayList<>(); result.add(createErrorMessage(activity, R.string.no_dictionaries_available)); return result; - } else { - final String systemLocaleString = Locale.getDefault().toString(); - final TreeMap<String, WordListPreference> prefMap = new TreeMap<>(); - final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); - final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); - final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); - final int descriptionIndex = - cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN); - final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN); - final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN); - do { - final String wordlistId = cursor.getString(idIndex); - final int version = cursor.getInt(versionIndex); - final String localeString = cursor.getString(localeIndex); - final Locale locale = new Locale(localeString); - final String description = cursor.getString(descriptionIndex); - final int status = cursor.getInt(statusIndex); - final int matchLevel = - LocaleUtils.getMatchLevel(systemLocaleString, localeString); - final String matchLevelString = - LocaleUtils.getMatchLevelSortedString(matchLevel); - final int filesize = cursor.getInt(filesizeIndex); - // The key is sorted in lexicographic order, according to the match level, then - // the description. - final String key = matchLevelString + "." + description + "." + wordlistId; - final WordListPreference existingPref = prefMap.get(key); - if (null == existingPref || existingPref.hasPriorityOver(status)) { - final WordListPreference oldPreference = mCurrentPreferenceMap.get(key); - final WordListPreference pref; - if (null != oldPreference - && oldPreference.mVersion == version - && oldPreference.hasStatus(status) - && oldPreference.mLocale.equals(locale)) { - // If the old preference has all the new attributes, reuse it. Ideally, - // we should reuse the old pref even if its status is different and call - // setStatus here, but setStatus calls Preference#setSummary() which - // needs to be done on the UI thread and we're not on the UI thread - // here. We could do all this work on the UI thread, but in this case - // it's probably lighter to stay on a background thread and throw this - // old preference out. - pref = oldPreference; - } else { - // Otherwise, discard it and create a new one instead. - // TODO: when the status is different from the old one, we need to - // animate the old one out before animating the new one in. - pref = new WordListPreference(activity, mDictionaryListInterfaceState, - mClientId, wordlistId, version, locale, description, status, - filesize); - } - prefMap.put(key, pref); - } - } while (cursor.moveToNext()); - mCurrentPreferenceMap = prefMap; - return prefMap.values(); } + final String systemLocaleString = Locale.getDefault().toString(); + final TreeMap<String, WordListPreference> prefMap = new TreeMap<>(); + final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); + final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); + final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); + final int descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN); + final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN); + final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN); + do { + final String wordlistId = cursor.getString(idIndex); + final int version = cursor.getInt(versionIndex); + final String localeString = cursor.getString(localeIndex); + final Locale locale = new Locale(localeString); + final String description = cursor.getString(descriptionIndex); + final int status = cursor.getInt(statusIndex); + final int matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString); + final String matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel); + final int filesize = cursor.getInt(filesizeIndex); + // The key is sorted in lexicographic order, according to the match level, then + // the description. + final String key = matchLevelString + "." + description + "." + wordlistId; + final WordListPreference existingPref = prefMap.get(key); + if (null == existingPref || existingPref.hasPriorityOver(status)) { + final WordListPreference oldPreference = mCurrentPreferenceMap.get(key); + final WordListPreference pref; + if (null != oldPreference + && oldPreference.mVersion == version + && oldPreference.hasStatus(status) + && oldPreference.mLocale.equals(locale)) { + // If the old preference has all the new attributes, reuse it. Ideally, + // we should reuse the old pref even if its status is different and call + // setStatus here, but setStatus calls Preference#setSummary() which + // needs to be done on the UI thread and we're not on the UI thread + // here. We could do all this work on the UI thread, but in this case + // it's probably lighter to stay on a background thread and throw this + // old preference out. + pref = oldPreference; + } else { + // Otherwise, discard it and create a new one instead. + // TODO: when the status is different from the old one, we need to + // animate the old one out before animating the new one in. + pref = new WordListPreference(activity, mDictionaryListInterfaceState, + mClientId, wordlistId, version, locale, description, status, + filesize); + } + prefMap.put(key, pref); + } + } while (cursor.moveToNext()); + mCurrentPreferenceMap = prefMap; + return prefMap.values(); } finally { cursor.close(); } @@ -384,8 +394,13 @@ public final class DictionarySettingsFragment extends PreferenceFragment private void cancelRefresh() { UpdateHandler.unregisterUpdateEventListener(this); final Context context = getActivity(); - UpdateHandler.cancelUpdate(context, mClientId); - stopLoadingAnimation(); + new Thread("cancelByHand") { + @Override + public void run() { + UpdateHandler.cancelUpdate(context, mClientId); + stopLoadingAnimation(); + } + }.start(); } private void startLoadingAnimation() { @@ -396,26 +411,28 @@ public final class DictionarySettingsFragment extends PreferenceFragment if (null != mUpdateNowMenu) mUpdateNowMenu.setTitle(R.string.cancel); } - private void stopLoadingAnimation() { + void stopLoadingAnimation() { final View preferenceView = getView(); final Activity activity = getActivity(); if (null == activity) return; + final View loadingView = mLoadingView; + final MenuItem updateNowMenu = mUpdateNowMenu; activity.runOnUiThread(new Runnable() { - @Override - public void run() { - mLoadingView.setVisibility(View.GONE); - preferenceView.setVisibility(View.VISIBLE); - mLoadingView.startAnimation(AnimationUtils.loadAnimation( - getActivity(), android.R.anim.fade_out)); - preferenceView.startAnimation(AnimationUtils.loadAnimation( - getActivity(), android.R.anim.fade_in)); - // The menu is created by the framework asynchronously after the activity, - // which means it's possible to have the activity running but the menu not - // created yet - hence the necessity for a null check here. - if (null != mUpdateNowMenu) { - mUpdateNowMenu.setTitle(R.string.check_for_updates_now); - } + @Override + public void run() { + loadingView.setVisibility(View.GONE); + preferenceView.setVisibility(View.VISIBLE); + loadingView.startAnimation(AnimationUtils.loadAnimation( + activity, android.R.anim.fade_out)); + preferenceView.startAnimation(AnimationUtils.loadAnimation( + activity, android.R.anim.fade_in)); + // The menu is created by the framework asynchronously after the activity, + // which means it's possible to have the activity running but the menu not + // created yet - hence the necessity for a null check here. + if (null != updateNowMenu) { + updateNowMenu.setTitle(R.string.check_for_updates_now); } - }); + } + }); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadIdAndStartDate.java b/java/src/com/android/inputmethod/dictionarypack/DownloadIdAndStartDate.java new file mode 100644 index 000000000..6247a15e2 --- /dev/null +++ b/java/src/com/android/inputmethod/dictionarypack/DownloadIdAndStartDate.java @@ -0,0 +1,29 @@ +/* + * 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.dictionarypack; + +/** + * A simple container of download ID and download start date. + */ +public class DownloadIdAndStartDate { + public final long mId; + public final long mStartDate; + public DownloadIdAndStartDate(final long id, final long startDate) { + mId = id; + mStartDate = startDate; + } +} diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java b/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java index d3c0a910f..91ed673ae 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java +++ b/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java @@ -24,9 +24,11 @@ import android.view.View; import android.widget.Button; import android.widget.TextView; +import com.android.inputmethod.annotations.ExternallyReferenced; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.common.LocaleUtils; -import java.util.Locale; +import javax.annotation.Nullable; /** * This implements the dialog for asking the user whether it's okay to download dictionaries over @@ -52,22 +54,30 @@ public final class DownloadOverMeteredDialog extends Activity { setTexts(localeString, size); } - private void setTexts(final String localeString, final long size) { + private void setTexts(@Nullable final String localeString, final long size) { final String promptFormat = getString(R.string.should_download_over_metered_prompt); final String allowButtonFormat = getString(R.string.download_over_metered); - final Locale locale = LocaleUtils.constructLocaleFromString(localeString); - final String language = (null == locale ? "" : locale.getDisplayLanguage()); + final String language = (null == localeString) ? "" + : LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage(); final TextView prompt = (TextView)findViewById(R.id.download_over_metered_prompt); prompt.setText(Html.fromHtml(String.format(promptFormat, language))); final Button allowButton = (Button)findViewById(R.id.allow_button); allowButton.setText(String.format(allowButtonFormat, ((float)size)/(1024*1024))); } + // This method is externally referenced from layout/download_over_metered.xml using onClick + // attribute of Button. + @ExternallyReferenced + @SuppressWarnings("unused") public void onClickDeny(final View v) { UpdateHandler.setDownloadOverMeteredSetting(this, false); finish(); } + // This method is externally referenced from layout/download_over_metered.xml using onClick + // attribute of Button. + @ExternallyReferenced + @SuppressWarnings("unused") public void onClickAllow(final View v) { UpdateHandler.setDownloadOverMeteredSetting(this, true); UpdateHandler.installIfNeverRequested(this, mClientId, mWordListToDownload, diff --git a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java b/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java deleted file mode 100644 index 4f0805c5c..000000000 --- a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (C) 2011 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.dictionarypack; - -import android.content.res.Configuration; -import android.content.res.Resources; -import android.text.TextUtils; - -import java.util.HashMap; -import java.util.Locale; - -/** - * A class to help with handling Locales in string form. - * - * This file has the same meaning and features (and shares all of its code) with the one with the - * same name in Latin IME. They need to be kept synchronized; for any update/bugfix to - * this file, consider also updating/fixing the version in Latin IME. - */ -public final class LocaleUtils { - private LocaleUtils() { - // Intentional empty constructor for utility class. - } - - // Locale match level constants. - // A higher level of match is guaranteed to have a higher numerical value. - // Some room is left within constants to add match cases that may arise necessary - // in the future, for example differentiating between the case where the countries - // are both present and different, and the case where one of the locales does not - // specify the countries. This difference is not needed now. - - // Nothing matches. - public static final int LOCALE_NO_MATCH = 0; - // The languages matches, but the country are different. Or, the reference locale requires a - // country and the tested locale does not have one. - public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3; - // The languages and country match, but the variants are different. Or, the reference locale - // requires a variant and the tested locale does not have one. - public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6; - // The required locale is null or empty so it will accept anything, and the tested locale - // is non-null and non-empty. - public static final int LOCALE_ANY_MATCH = 10; - // The language matches, and the tested locale specifies a country but the reference locale - // does not require one. - public static final int LOCALE_LANGUAGE_MATCH = 15; - // The language and the country match, and the tested locale specifies a variant but the - // reference locale does not require one. - public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20; - // The compared locales are fully identical. This is the best match level. - public static final int LOCALE_FULL_MATCH = 30; - - // The level at which a match is "normally" considered a locale match with standard algorithms. - // Don't use this directly, use #isMatch to test. - private static final int LOCALE_MATCH = LOCALE_ANY_MATCH; - - // Make this match the maximum match level. If this evolves to have more than 2 digits - // when written in base 10, also adjust the getMatchLevelSortedString method. - private static final int MATCH_LEVEL_MAX = 30; - - /** - * Return how well a tested locale matches a reference locale. - * - * This will check the tested locale against the reference locale and return a measure of how - * a well it matches the reference. The general idea is that the tested locale has to match - * every specified part of the required locale. A full match occur when they are equal, a - * partial match when the tested locale agrees with the reference locale but is more specific, - * and a difference when the tested locale does not comply with all requirements from the - * reference locale. - * In more detail, if the reference locale specifies at least a language and the testedLocale - * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the - * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH - * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and - * tested locale agree on the language, but not on the country, - * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country, - * and LOCALE_LANGUAGE_MATCH otherwise. - * If they agree on both the language and the country, but not on the variant, - * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale - * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches, - * LOCALE_FULL_MATCH is returned. - * Examples: - * en <=> en_US => LOCALE_LANGUAGE_MATCH - * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER - * en_US_POSIX <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER - * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH - * sp_US <=> en_US => LOCALE_NO_MATCH - * de <=> de => LOCALE_FULL_MATCH - * en_US <=> en_US => LOCALE_FULL_MATCH - * "" <=> en_US => LOCALE_ANY_MATCH - * - * @param referenceLocale the reference locale to test against. - * @param testedLocale the locale to test. - * @return a constant that measures how well the tested locale matches the reference locale. - */ - public static int getMatchLevel(final String referenceLocale, final String testedLocale) { - if (TextUtils.isEmpty(referenceLocale)) { - return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH; - } - if (null == testedLocale) return LOCALE_NO_MATCH; - final String[] referenceParams = referenceLocale.split("_", 3); - final String[] testedParams = testedLocale.split("_", 3); - // By spec of String#split, [0] cannot be null and length cannot be 0. - if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH; - switch (referenceParams.length) { - case 1: - return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH; - case 2: - if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; - if (!referenceParams[1].equals(testedParams[1])) - return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; - if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH; - return LOCALE_FULL_MATCH; - case 3: - if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; - if (!referenceParams[1].equals(testedParams[1])) - return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; - if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; - if (!referenceParams[2].equals(testedParams[2])) - return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; - return LOCALE_FULL_MATCH; - } - // It should be impossible to come here - return LOCALE_NO_MATCH; - } - - /** - * Return a string that represents this match level, with better matches first. - * - * The strings are sorted in lexicographic order: a better match will always be less than - * a worse match when compared together. - */ - public static String getMatchLevelSortedString(final int matchLevel) { - // This works because the match levels are 0~99 (actually 0~30) - // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel - return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel); - } - - /** - * Find out whether a match level should be considered a match. - * - * This method takes a match level as returned by the #getMatchLevel method, and returns whether - * it should be considered a match in the usual sense with standard Locale functions. - * - * @param level the match level, as returned by getMatchLevel. - * @return whether this is a match or not. - */ - public static boolean isMatch(final int level) { - return LOCALE_MATCH <= level; - } - - /** - * Sets the system locale for this process. - * - * @param res the resources to use. Pass current resources. - * @param newLocale the locale to change to. - * @return the old locale. - */ - public static Locale setSystemLocale(final Resources res, final Locale newLocale) { - final Configuration conf = res.getConfiguration(); - final Locale saveLocale = conf.locale; - conf.locale = newLocale; - res.updateConfiguration(conf, res.getDisplayMetrics()); - return saveLocale; - } - - private static final HashMap<String, Locale> sLocaleCache = new HashMap<>(); - - /** - * Creates a locale from a string specification. - */ - public static Locale constructLocaleFromString(final String localeStr) { - if (localeStr == null) - return null; - synchronized (sLocaleCache) { - if (sLocaleCache.containsKey(localeStr)) - return sLocaleCache.get(localeStr); - Locale retval = null; - String[] localeParams = localeStr.split("_", 3); - if (localeParams.length == 1) { - retval = new Locale(localeParams[0]); - } else if (localeParams.length == 2) { - retval = new Locale(localeParams[0], localeParams[1]); - } else if (localeParams.length == 3) { - retval = new Locale(localeParams[0], localeParams[1], localeParams[2]); - } - if (retval != null) { - sLocaleCache.put(localeStr, retval); - } - return retval; - } - } -} diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java index 17dd781d5..a2789cc1a 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java @@ -47,10 +47,14 @@ 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; + // This MUST be increased every time the dictionary pack metadata URL changes. + private static final int CURRENT_METADATA_DATABASE_VERSION = 14; 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 +72,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 +103,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 +131,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 +148,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 +227,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { createClientTable(db); } - private void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db, final String clientId) { + private static void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db) { try { db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM " + METADATA_TABLE_NAME + " LIMIT 0;"); @@ -230,6 +238,17 @@ public class MetadataDbHelper extends SQLiteOpenHelper { } } + private static 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. @@ -245,6 +264,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) { @@ -280,7 +301,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); } /** @@ -408,18 +436,18 @@ public class MetadataDbHelper extends SQLiteOpenHelper { * * @param context a context instance to open the database on * @param uri the URI to retrieve the metadata download ID of - * @return the metadata download ID, or NOT_AN_ID if no download is in progress + * @return the download id and start date, or null if the URL is not known */ - public static long getMetadataDownloadIdForURI(final Context context, - final String uri) { + public static DownloadIdAndStartDate getMetadataDownloadIdAndStartDateForURI( + final Context context, final String uri) { SQLiteDatabase defaultDb = getDb(context, null); final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, - new String[] { CLIENT_PENDINGID_COLUMN }, + new String[] { CLIENT_PENDINGID_COLUMN, CLIENT_LAST_UPDATE_DATE_COLUMN }, CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri }, null, null, null, null); try { - if (!cursor.moveToFirst()) return UpdateHandler.NOT_AN_ID; - return cursor.getInt(0); // Only one column, return it + if (!cursor.moveToFirst()) return null; + return new DownloadIdAndStartDate(cursor.getInt(0), cursor.getLong(1)); } finally { cursor.close(); } @@ -452,8 +480,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 +493,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 +531,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 +583,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 +709,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 +747,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(); @@ -884,6 +925,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { final long downloadId) { final ContentValues values = new ContentValues(); values.put(CLIENT_PENDINGID_COLUMN, downloadId); + values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis()); final SQLiteDatabase defaultDb = getDb(context, ""); final Cursor cursor = MetadataDbHelper.queryClientIds(context); if (null == cursor) return; @@ -1085,4 +1127,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..329b9f62e 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; @@ -29,9 +30,6 @@ import java.util.List; * Helper class to easy up manipulation of dictionary pack metadata. */ public class MetadataHandler { - @SuppressWarnings("unused") - private static final String TAG = "DictionaryProvider:" + MetadataHandler.class.getSimpleName(); - // The canonical file name for metadata. This is not the name of a real file on the // device, but a symbolic name used in the database and in metadata handling. It is never // tested against, only used for human-readability as the file name for the metadata. @@ -55,6 +53,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 +69,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 +102,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/PrivateLog.java b/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java index 67dd7b9b7..bb64721d5 100644 --- a/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java +++ b/java/src/com/android/inputmethod/dictionarypack/PrivateLog.java @@ -43,8 +43,8 @@ public class PrivateLog { + COLUMN_DATE + " TEXT," + COLUMN_EVENT + " TEXT);"; - private static final SimpleDateFormat sDateFormat = - new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US); + static final SimpleDateFormat sDateFormat = new SimpleDateFormat( + "yyyy/MM/dd HH:mm:ss", Locale.ROOT); private static PrivateLog sInstance = new PrivateLog(); private static DebugHelper sDebugHelper = null; @@ -62,9 +62,9 @@ public class PrivateLog { } } - private static class DebugHelper extends SQLiteOpenHelper { + static class DebugHelper extends SQLiteOpenHelper { - private DebugHelper(final Context context) { + DebugHelper(final Context context) { super(context, LOG_DATABASE_NAME, null, LOG_DATABASE_VERSION); } @@ -84,7 +84,7 @@ public class PrivateLog { insert(db, "Upgrade finished"); } - private static void insert(SQLiteDatabase db, String event) { + static void insert(SQLiteDatabase db, String event) { if (!DEBUG) return; final ContentValues c = new ContentValues(2); c.put(COLUMN_DATE, sDateFormat.format(new Date(System.currentTimeMillis()))); diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index 6fbca44c5..30ff0b8ee 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; @@ -40,6 +39,8 @@ 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; +import com.android.inputmethod.latin.makedict.FormatSpec; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.DebugLogUtils; @@ -56,10 +57,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Locale; import java.util.Set; import java.util.TreeSet; +import javax.annotation.Nullable; + /** * Handler for the update process. * @@ -77,7 +79,8 @@ public final class UpdateHandler { // DownloadManager uses as an ID numbers returned out of an AUTOINCREMENT column // in SQLite, so it should never return anything < 0. public static final int NOT_AN_ID = -1; - public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION = 2; + public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION = + FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION; // Arbitrary. Probably good if it's a power of 2, and a couple thousand bytes long. private static final int FILE_COPY_BUFFER_SIZE = 8192; @@ -252,12 +255,16 @@ public final class UpdateHandler { res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); - cancelUpdateWithDownloadManager(context, metadataUri, manager); + if (maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, + DictionaryService.NO_CANCEL_DOWNLOAD_PERIOD_MILLIS)) { + // We already have a recent download in progress. Don't register a new download. + return; + } final long downloadId; synchronized (sSharedIdProtector) { downloadId = manager.enqueue(metadataRequest); DebugLogUtils.l("Metadata download requested with id", downloadId); - // If there is already a download in progress, it's been there for a while and + // If there is still a download in progress, it's been there for a while and // there is probably something wrong with download manager. It's best to just // overwrite the id and request it again. If the old one happens to finish // anyway, we don't know about its ID any more, so the downloadFinished @@ -268,21 +275,29 @@ public final class UpdateHandler { } /** - * Cancels downloading a file, if there is one for this URI. + * Cancels downloading a file if there is one for this URI and it's too long. * * If we are not currently downloading the file at this URI, this is a no-op. * * @param context the context to open the database on * @param metadataUri the URI to cancel * @param manager an wrapped instance of DownloadManager + * @param graceTime if there was a download started less than this many milliseconds, don't + * cancel and return true + * @return whether the download is still active */ - private static void cancelUpdateWithDownloadManager(final Context context, - final String metadataUri, final DownloadManagerWrapper manager) { + private static boolean maybeCancelUpdateAndReturnIfStillRunning(final Context context, + final String metadataUri, final DownloadManagerWrapper manager, final long graceTime) { synchronized (sSharedIdProtector) { - final long metadataDownloadId = - MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri); - if (NOT_AN_ID == metadataDownloadId) return; - manager.remove(metadataDownloadId); + final DownloadIdAndStartDate metadataDownloadIdAndStartDate = + MetadataDbHelper.getMetadataDownloadIdAndStartDateForURI(context, metadataUri); + if (null == metadataDownloadIdAndStartDate) return false; + if (NOT_AN_ID == metadataDownloadIdAndStartDate.mId) return false; + if (metadataDownloadIdAndStartDate.mStartDate + graceTime + > System.currentTimeMillis()) { + return true; + } + manager.remove(metadataDownloadIdAndStartDate.mId); writeMetadataDownloadId(context, metadataUri, NOT_AN_ID); } // Consider a cancellation as a failure. As such, inform listeners that the download @@ -290,6 +305,7 @@ public final class UpdateHandler { for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { listener.downloadedMetadata(false); } + return false; } /** @@ -304,7 +320,7 @@ public final class UpdateHandler { public static void cancelUpdate(final Context context, final String clientId) { final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId); - cancelUpdateWithDownloadManager(context, metadataUri, manager); + maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, 0 /* graceTime */); } /** @@ -388,7 +404,7 @@ public final class UpdateHandler { // If any of these is metadata, we should update the DB boolean hasMetadata = false; for (DownloadRecord record : downloadRecords) { - if (null == record.mAttributes) { + if (record.isMetadata()) { hasMetadata = true; break; } @@ -433,6 +449,8 @@ 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; @@ -447,9 +465,15 @@ public final class UpdateHandler { 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); } @@ -738,19 +762,22 @@ public final class UpdateHandler { * @return an ordered list of runnables to be called to upgrade. */ private static ActionBatch compareMetadataForUpgrade(final Context context, - final String clientId, List<WordListMetadata> from, List<WordListMetadata> to) { + final String clientId, @Nullable final List<WordListMetadata> from, + @Nullable final List<WordListMetadata> to) { final ActionBatch actions = new ActionBatch(); // Upgrade existing word lists DebugLogUtils.l("Comparing dictionaries"); final Set<String> wordListIds = new TreeSet<>(); // TODO: Can these be null? - if (null == from) from = new ArrayList<>(); - if (null == to) to = new ArrayList<>(); - for (WordListMetadata wlData : from) wordListIds.add(wlData.mId); - for (WordListMetadata wlData : to) wordListIds.add(wlData.mId); + final List<WordListMetadata> fromList = (from == null) ? new ArrayList<WordListMetadata>() + : from; + final List<WordListMetadata> toList = (to == null) ? new ArrayList<WordListMetadata>() + : to; + for (WordListMetadata wlData : fromList) wordListIds.add(wlData.mId); + for (WordListMetadata wlData : toList) wordListIds.add(wlData.mId); for (String id : wordListIds) { - final WordListMetadata currentInfo = MetadataHandler.findWordListById(from, id); - final WordListMetadata metadataInfo = MetadataHandler.findWordListById(to, id); + final WordListMetadata currentInfo = MetadataHandler.findWordListById(fromList, id); + final WordListMetadata metadataInfo = MetadataHandler.findWordListById(toList, id); // TODO: Remove the following unnecessary check, since we are now doing the filtering // inside findWordListById. final WordListMetadata newInfo = null == metadataInfo @@ -785,6 +812,10 @@ public final class UpdateHandler { } else { final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); if (newInfo.mVersion == currentInfo.mVersion) { + if (TextUtils.equals(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)); @@ -797,7 +828,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, false)); + 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 @@ -856,8 +888,8 @@ public final class UpdateHandler { // None of those are expected to happen, but just in case... if (null == notificationIntent || null == notificationManager) return; - final Locale locale = LocaleUtils.constructLocaleFromString(localeString); - final String language = (null == locale ? "" : locale.getDisplayLanguage()); + final String language = (null == localeString) ? "" + : LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage(); final String titleFormat = context.getString(R.string.dict_available_notification_title); final String notificationTitle = String.format(titleFormat, language); final Notification.Builder builder = new Notification.Builder(context) @@ -950,8 +982,10 @@ 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(); - actions.add(new ActionBatch.StartDownloadAction(clientId, - WordListMetadata.createFromContentValues(installCandidate), false)); + 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 @@ -987,17 +1021,19 @@ 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, - allowDownloadOnMeteredData)); + 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); } @@ -1022,13 +1058,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 +1087,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 +1112,48 @@ 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, CommonPreferences.isForceDownloadDict(context))); + 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/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java index aea16af0d..500e39e0e 100644 --- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java +++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java @@ -38,45 +38,39 @@ import java.util.Locale; * enable or delete it as appropriate for the current state of the word list. */ public final class WordListPreference extends Preference { - static final private String TAG = WordListPreference.class.getSimpleName(); + private static final String TAG = WordListPreference.class.getSimpleName(); // What to display in the "status" field when we receive unknown data as a status from // the content provider. Empty string sounds sensible. - static final private String NO_STATUS_MESSAGE = ""; + private static final String NO_STATUS_MESSAGE = ""; /// Actions - static final private int ACTION_UNKNOWN = 0; - static final private int ACTION_ENABLE_DICT = 1; - static final private int ACTION_DISABLE_DICT = 2; - static final private int ACTION_DELETE_DICT = 3; + private static final int ACTION_UNKNOWN = 0; + private static final int ACTION_ENABLE_DICT = 1; + private static final int ACTION_DISABLE_DICT = 2; + private static final int ACTION_DELETE_DICT = 3; // Members - // The context to get resources - final Context mContext; - // The id of the client for which this preference is. - final String mClientId; // The metadata word list id and version of this word list. public final String mWordlistId; public final int mVersion; public final Locale mLocale; public final String mDescription; + + // The id of the client for which this preference is. + private final String mClientId; // The status private int mStatus; // The size of the dictionary file private final int mFilesize; private final DictionaryListInterfaceState mInterfaceState; - private final OnWordListPreferenceClick mPreferenceClickHandler = - new OnWordListPreferenceClick(); - private final OnActionButtonClick mActionButtonClickHandler = - new OnActionButtonClick(); public WordListPreference(final Context context, final DictionaryListInterfaceState dictionaryListInterfaceState, final String clientId, final String wordlistId, final int version, final Locale locale, final String description, final int status, final int filesize) { super(context, null); - mContext = context; mInterfaceState = dictionaryListInterfaceState; mClientId = clientId; mVersion = version; @@ -116,22 +110,23 @@ public final class WordListPreference extends Preference { } private String getSummary(final int status) { + final Context context = getContext(); switch (status) { - // If we are deleting the word list, for the user it's like it's already deleted. - // It should be reinstallable. Exposing to the user the whole complexity of - // the delayed deletion process between the dictionary pack and Android Keyboard - // would only be confusing. - case MetadataDbHelper.STATUS_DELETING: - case MetadataDbHelper.STATUS_AVAILABLE: - return mContext.getString(R.string.dictionary_available); - case MetadataDbHelper.STATUS_DOWNLOADING: - return mContext.getString(R.string.dictionary_downloading); - case MetadataDbHelper.STATUS_INSTALLED: - return mContext.getString(R.string.dictionary_installed); - case MetadataDbHelper.STATUS_DISABLED: - return mContext.getString(R.string.dictionary_disabled); - default: - return NO_STATUS_MESSAGE; + // If we are deleting the word list, for the user it's like it's already deleted. + // It should be reinstallable. Exposing to the user the whole complexity of + // the delayed deletion process between the dictionary pack and Android Keyboard + // would only be confusing. + case MetadataDbHelper.STATUS_DELETING: + case MetadataDbHelper.STATUS_AVAILABLE: + return context.getString(R.string.dictionary_available); + case MetadataDbHelper.STATUS_DOWNLOADING: + return context.getString(R.string.dictionary_downloading); + case MetadataDbHelper.STATUS_INSTALLED: + return context.getString(R.string.dictionary_installed); + case MetadataDbHelper.STATUS_DISABLED: + return context.getString(R.string.dictionary_disabled); + default: + return NO_STATUS_MESSAGE; } } @@ -154,7 +149,7 @@ public final class WordListPreference extends Preference { { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT } }; - private int getButtonSwitcherStatus(final int status) { + static int getButtonSwitcherStatus(final int status) { if (status >= sStatusActionList.length) { Log.e(TAG, "Unknown status " + status); return ButtonSwitcher.STATUS_NO_BUTTON; @@ -162,7 +157,7 @@ public final class WordListPreference extends Preference { return sStatusActionList[status][0]; } - private static int getActionIdFromStatusAndMenuEntry(final int status) { + static int getActionIdFromStatusAndMenuEntry(final int status) { if (status >= sStatusActionList.length) { Log.e(TAG, "Unknown status " + status); return ACTION_UNKNOWN; @@ -171,9 +166,10 @@ public final class WordListPreference extends Preference { } private void disableDict() { - SharedPreferences prefs = CommonPreferences.getCommonPreferences(mContext); + final Context context = getContext(); + final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); CommonPreferences.disable(prefs, mWordlistId); - UpdateHandler.markAsUnused(mContext, mClientId, mWordlistId, mVersion, mStatus); + UpdateHandler.markAsUnused(context, mClientId, mWordlistId, mVersion, mStatus); if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) { setStatus(MetadataDbHelper.STATUS_AVAILABLE); } else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) { @@ -184,11 +180,13 @@ public final class WordListPreference extends Preference { Log.e(TAG, "Unexpected state of the word list for disabling " + mStatus); } } + private void enableDict() { - SharedPreferences prefs = CommonPreferences.getCommonPreferences(mContext); + final Context context = getContext(); + final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); CommonPreferences.enable(prefs, mWordlistId); // Explicit enabling by the user : allow downloading on metered data connection. - UpdateHandler.markAsUsed(mContext, mClientId, mWordlistId, mVersion, mStatus, true); + UpdateHandler.markAsUsed(context, mClientId, mWordlistId, mVersion, mStatus, true); if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) { setStatus(MetadataDbHelper.STATUS_DOWNLOADING); } else if (MetadataDbHelper.STATUS_DISABLED == mStatus @@ -203,11 +201,13 @@ public final class WordListPreference extends Preference { Log.e(TAG, "Unexpected state of the word list for enabling " + mStatus); } } + private void deleteDict() { - SharedPreferences prefs = CommonPreferences.getCommonPreferences(mContext); + final Context context = getContext(); + final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); CommonPreferences.disable(prefs, mWordlistId); setStatus(MetadataDbHelper.STATUS_DELETING); - UpdateHandler.markAsDeleting(mContext, mClientId, mWordlistId, mVersion, mStatus); + UpdateHandler.markAsDeleting(context, mClientId, mWordlistId, mVersion, mStatus); } @Override @@ -225,8 +225,8 @@ public final class WordListPreference extends Preference { status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE); progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE); - final ButtonSwitcher buttonSwitcher = - (ButtonSwitcher)view.findViewById(R.id.wordlist_button_switcher); + final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)view.findViewById( + R.id.wordlist_button_switcher); // We need to clear the state of the button switcher, because we reuse views; if we didn't // reset it would animate from whatever its old state was. buttonSwitcher.reset(mInterfaceState); @@ -244,63 +244,67 @@ public final class WordListPreference extends Preference { // The button is closed. buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON); } - buttonSwitcher.setInternalOnClickListener(mActionButtonClickHandler); - view.setOnClickListener(mPreferenceClickHandler); + buttonSwitcher.setInternalOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + onActionButtonClicked(); + } + }); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + onWordListClicked(v); + } + }); } - private class OnWordListPreferenceClick implements View.OnClickListener { - @Override - public void onClick(final View v) { - // Note : v is the preference view - final ViewParent parent = v.getParent(); - // Just in case something changed in the framework, test for the concrete class - if (!(parent instanceof ListView)) return; - final ListView listView = (ListView)parent; - final int indexToOpen; - // Close all first, we'll open back any item that needs to be open. - final boolean wasOpen = mInterfaceState.isOpen(mWordlistId); - mInterfaceState.closeAll(); - if (wasOpen) { - // This button being shown. Take note that we don't want to open any button in the - // loop below. - indexToOpen = -1; + void onWordListClicked(final View v) { + // Note : v is the preference view + final ViewParent parent = v.getParent(); + // Just in case something changed in the framework, test for the concrete class + if (!(parent instanceof ListView)) return; + final ListView listView = (ListView)parent; + final int indexToOpen; + // Close all first, we'll open back any item that needs to be open. + final boolean wasOpen = mInterfaceState.isOpen(mWordlistId); + mInterfaceState.closeAll(); + if (wasOpen) { + // This button being shown. Take note that we don't want to open any button in the + // loop below. + indexToOpen = -1; + } else { + // This button was not being shown. Open it, and remember the index of this + // child as the one to open in the following loop. + mInterfaceState.setOpen(mWordlistId, mStatus); + indexToOpen = listView.indexOfChild(v); + } + final int lastDisplayedIndex = + listView.getLastVisiblePosition() - listView.getFirstVisiblePosition(); + // The "lastDisplayedIndex" is actually displayed, hence the <= + for (int i = 0; i <= lastDisplayedIndex; ++i) { + final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)listView.getChildAt(i) + .findViewById(R.id.wordlist_button_switcher); + if (i == indexToOpen) { + buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus)); } else { - // This button was not being shown. Open it, and remember the index of this - // child as the one to open in the following loop. - mInterfaceState.setOpen(mWordlistId, mStatus); - indexToOpen = listView.indexOfChild(v); - } - final int lastDisplayedIndex = - listView.getLastVisiblePosition() - listView.getFirstVisiblePosition(); - // The "lastDisplayedIndex" is actually displayed, hence the <= - for (int i = 0; i <= lastDisplayedIndex; ++i) { - final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)listView.getChildAt(i) - .findViewById(R.id.wordlist_button_switcher); - if (i == indexToOpen) { - buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus)); - } else { - buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON); - } + buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON); } } } - private class OnActionButtonClick implements View.OnClickListener { - @Override - public void onClick(final View v) { - switch (getActionIdFromStatusAndMenuEntry(mStatus)) { - case ACTION_ENABLE_DICT: - enableDict(); - break; - case ACTION_DISABLE_DICT: - disableDict(); - break; - case ACTION_DELETE_DICT: - deleteDict(); - break; - default: - Log.e(TAG, "Unknown menu item pressed"); - } + void onActionButtonClicked() { + switch (getActionIdFromStatusAndMenuEntry(mStatus)) { + case ACTION_ENABLE_DICT: + enableDict(); + break; + case ACTION_DISABLE_DICT: + disableDict(); + break; + case ACTION_DELETE_DICT: + deleteDict(); + break; + default: + Log.e(TAG, "Unknown menu item pressed"); } } } |