diff options
Diffstat (limited to 'java/src/com/android/inputmethod/dictionarypack')
17 files changed, 479 insertions, 309 deletions
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java index 3d294acd7..3aa026e77 100644 --- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java +++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java @@ -120,9 +120,10 @@ public final class ActionBatch { if (MetadataDbHelper.STATUS_DOWNLOADING == status) { // The word list is still downloading. Cancel the download and revert the // word list status to "available". - manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); + manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); - } else if (MetadataDbHelper.STATUS_AVAILABLE != status) { + } else if (MetadataDbHelper.STATUS_AVAILABLE != status + && MetadataDbHelper.STATUS_RETRYING != status) { // Should never happen Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " + status + " for an upgrade action. Fall back to download."); @@ -325,8 +326,8 @@ public final class ActionBatch { mWordList.mId, mWordList.mLocale, mWordList.mDescription, null == mWordList.mLocalFilename ? "" : mWordList.mLocalFilename, mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, + mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Insert 'available' record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); @@ -374,9 +375,9 @@ public final class ActionBatch { final ContentValues values = MetadataDbHelper.makeContentValues(0, MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED, mWordList.mId, mWordList.mLocale, mWordList.mDescription, - "", mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + "", mWordList.mRemoteFilename, mWordList.mLastUpdate, + mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mRetryCount, + mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); @@ -417,8 +418,8 @@ public final class ActionBatch { mWordList.mId, mWordList.mLocale, mWordList.mDescription, oldValues.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN), mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, + mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Updating record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.update(MetadataDbHelper.METADATA_TABLE_NAME, values, diff --git a/java/src/com/android/inputmethod/dictionarypack/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/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..37fa76be7 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java @@ -255,10 +255,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 +318,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 +459,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..e9b634eec 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java @@ -76,19 +76,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. @@ -169,7 +179,7 @@ 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())) { // 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 @@ -188,16 +198,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,11 +233,11 @@ 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; + if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME_MILLIS)) return; UpdateHandler.tryUpdate(context, false); } 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..c2dc87900 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java @@ -31,7 +31,6 @@ 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; @@ -203,25 +202,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 +232,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 +269,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 +282,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(); } @@ -396,26 +385,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..f1633ff28 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java +++ b/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java @@ -24,6 +24,7 @@ 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 java.util.Locale; @@ -63,11 +64,19 @@ public final class DownloadOverMeteredDialog extends Activity { 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/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java index 17dd781d5..db4315f8f 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java @@ -47,10 +47,13 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // used to identify the versions for upgrades. This should never change going forward. private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6; // The current database version. - private static final int CURRENT_METADATA_DATABASE_VERSION = 9; + private static final int CURRENT_METADATA_DATABASE_VERSION = 10; private final static long NOT_A_DOWNLOAD_ID = -1; + // The number of retries allowed when attempting to download a broken dictionary. + public static final int DICTIONARY_RETRY_THRESHOLD = 2; + public static final String METADATA_TABLE_NAME = "pendingUpdates"; static final String CLIENT_TABLE_NAME = "clients"; public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID @@ -68,7 +71,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static final String FORMATVERSION_COLUMN = "formatversion"; public static final String FLAGS_COLUMN = "flags"; public static final String RAW_CHECKSUM_COLUMN = "rawChecksum"; - public static final int COLUMN_COUNT = 14; + public static final String RETRY_COUNT_COLUMN = "remainingRetries"; + public static final int COLUMN_COUNT = 15; private static final String CLIENT_CLIENT_ID_COLUMN = "clientid"; private static final String CLIENT_METADATA_URI_COLUMN = "uri"; @@ -98,6 +102,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // Deleting: the user marked this word list to be deleted, but it has not been yet because // Latin IME is not up yet. public static final int STATUS_DELETING = 5; + // Retry: dictionary got corrupted, so an attempt must be done to download & install it again. + public static final int STATUS_RETRYING = 6; // Types, for storing in the TYPE_COLUMN // This is metadata about what is available. @@ -124,6 +130,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { + FORMATVERSION_COLUMN + " INTEGER, " + FLAGS_COLUMN + " INTEGER, " + RAW_CHECKSUM_COLUMN + " TEXT," + + RETRY_COUNT_COLUMN + " INTEGER, " + "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));"; private static final String METADATA_CREATE_CLIENT_TABLE = "CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " (" @@ -140,7 +147,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN, LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN, - RAW_CHECKSUM_COLUMN }; + RAW_CHECKSUM_COLUMN, RETRY_COUNT_COLUMN }; // List of all client table columns. static final String[] CLIENT_TABLE_COLUMNS = { CLIENT_CLIENT_ID_COLUMN, CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN }; @@ -219,7 +226,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { createClientTable(db); } - private void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db, final String clientId) { + private static void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db) { try { db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM " + METADATA_TABLE_NAME + " LIMIT 0;"); @@ -230,6 +237,17 @@ public class MetadataDbHelper extends SQLiteOpenHelper { } } + private 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. @@ -280,7 +298,14 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // strengthen the system against corrupted dictionary files. // The most secure way to upgrade a database is to just test for the column presence, and // add it if it's not there. - addRawChecksumColumnUnlessPresent(db, mClientId); + addRawChecksumColumnUnlessPresent(db); + + // A retry count column that did not exist in the previous versions was added that + // corresponds to the number of download & installation attempts that have been made + // in order to strengthen the system recovery from corrupted dictionary files. + // The most secure way to upgrade a database is to just test for the column presence, and + // add it if it's not there. + addRetryCountColumnUnlessPresent(db); } /** @@ -408,18 +433,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 +477,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static ContentValues makeContentValues(final int pendingId, final int type, final int status, final String wordlistId, final String locale, final String description, final String filename, final String url, final long date, - final String rawChecksum, final String checksum, final long filesize, final int version, - final int formatVersion) { + final String rawChecksum, final String checksum, final int retryCount, + final long filesize, final int version, final int formatVersion) { final ContentValues result = new ContentValues(COLUMN_COUNT); result.put(PENDINGID_COLUMN, pendingId); result.put(TYPE_COLUMN, type); @@ -465,6 +490,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { result.put(REMOTE_FILENAME_COLUMN, url); result.put(DATE_COLUMN, date); result.put(RAW_CHECKSUM_COLUMN, rawChecksum); + result.put(RETRY_COUNT_COLUMN, retryCount); result.put(CHECKSUM_COLUMN, checksum); result.put(FILESIZE_COLUMN, filesize); result.put(VERSION_COLUMN, version); @@ -502,6 +528,9 @@ public class MetadataDbHelper extends SQLiteOpenHelper { if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0); // Raw checksum unknown unless specified if (null == result.get(RAW_CHECKSUM_COLUMN)) result.put(RAW_CHECKSUM_COLUMN, ""); + // Retry column 0 unless specified + if (null == result.get(RETRY_COUNT_COLUMN)) result.put(RETRY_COUNT_COLUMN, + DICTIONARY_RETRY_THRESHOLD); // Checksum unknown unless specified if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, ""); // No filesize unless specified @@ -551,6 +580,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { putIntResult(result, cursor, DATE_COLUMN); putStringResult(result, cursor, RAW_CHECKSUM_COLUMN); putStringResult(result, cursor, CHECKSUM_COLUMN); + putIntResult(result, cursor, RETRY_COUNT_COLUMN); putIntResult(result, cursor, FILESIZE_COLUMN); putIntResult(result, cursor, VERSION_COLUMN); putIntResult(result, cursor, FORMATVERSION_COLUMN); @@ -676,8 +706,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper { final String id, final int version) { final Cursor cursor = db.query(METADATA_TABLE_NAME, METADATA_TABLE_COLUMNS, - WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ?", - new String[] { id, Integer.toString(version) }, null, null, null); + WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ? AND " + + FORMATVERSION_COLUMN + "<= ?", + new String[] + { id, + Integer.toString(version), + Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION) + }, + null /* groupBy */, + null /* having */, + FORMATVERSION_COLUMN + " DESC"/* orderBy */); if (null == cursor) { return null; } @@ -706,7 +744,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { return null; } try { - // This is a lookup by primary key, so there can't be more than one result. + // Return the first result from the list of results. return getFirstLineAsContentValues(cursor); } finally { cursor.close(); @@ -884,6 +922,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 +1124,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..d59b7a545 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; @@ -60,6 +59,8 @@ import java.util.Locale; import java.util.Set; import java.util.TreeSet; +import javax.annotation.Nullable; + /** * Handler for the update process. * @@ -252,12 +253,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 +273,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 +303,7 @@ public final class UpdateHandler { for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { listener.downloadedMetadata(false); } + return false; } /** @@ -304,7 +318,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 +402,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; } @@ -738,19 +752,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 +802,10 @@ public final class UpdateHandler { } else { final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); if (newInfo.mVersion == currentInfo.mVersion) { + if (newInfo.mRemoteFilename == currentInfo.mRemoteFilename) { + // If the dictionary url hasn't changed, we should preserve the retryCount. + newInfo.mRetryCount = currentInfo.mRetryCount; + } // If it's the same id/version, we update the DB with the new values. // It doesn't matter too much if they didn't change. actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo)); @@ -987,16 +1008,17 @@ public final class UpdateHandler { public static void markAsUsed(final Context context, final String clientId, final String wordlistId, final int version, final int status, final boolean allowDownloadOnMeteredData) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - WordListMetadata wordList = MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + final ActionBatch actions = new ActionBatch(); if (MetadataDbHelper.STATUS_DISABLED == status || MetadataDbHelper.STATUS_DELETING == status) { - actions.add(new ActionBatch.EnableAction(clientId, wordList)); + actions.add(new ActionBatch.EnableAction(clientId, wordListMetaData)); } else if (MetadataDbHelper.STATUS_AVAILABLE == status) { - actions.add(new ActionBatch.StartDownloadAction(clientId, wordList, + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData, allowDownloadOnMeteredData)); } else { Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status); @@ -1022,13 +1044,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 +1073,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 +1098,47 @@ public final class UpdateHandler { */ public static void markAsDeleted(final Context context, final String clientId, final String wordlistId, final int version, final int status) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - final WordListMetadata wordList = - MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.FinishDeleteAction(clientId, wordList)); + actions.add(new ActionBatch.FinishDeleteAction(clientId, wordListMetaData)); actions.execute(context, new LogProblemReporter(TAG)); signalNewDictionaryState(context); } /** - * Marks the word list with the passed id as broken. - * - * This effectively deletes the entry from the metadata. It doesn't prevent the same - * word list to be downloaded again at a later time if the same or a new version is - * available the next time we download the metadata. + * Checks whether the word list should be downloaded again; in which case an download & + * installation attempt is made. Otherwise the word list is marked broken. * * @param context the context to open the database on. * @param clientId the id of the client. - * @param wordlistId the id of the word list to mark as broken. - * @param version the version of the word list to mark as deleted. + * @param wordlistId the id of the word list which is broken. + * @param version the version of the broken word list. */ - public static void markAsBroken(final Context context, final String clientId, + public static void markAsBrokenOrRetrying(final Context context, final String clientId, final String wordlistId, final int version) { - // TODO: do this on another thread to avoid blocking the UI. - MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), - wordlistId, version); + boolean isRetryPossible = MetadataDbHelper.maybeMarkEntryAsRetrying( + MetadataDbHelper.getDb(context, clientId), wordlistId, version); + + if (isRetryPossible) { + if (DEBUG) { + Log.d(TAG, "Attempting to download & install the wordlist again."); + } + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + final ActionBatch actions = new ActionBatch(); + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData, false)); + actions.execute(context, new LogProblemReporter(TAG)); + } else { + if (DEBUG) { + Log.d(TAG, "Retries for wordlist exhausted, deleting the wordlist from table."); + } + MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), + wordlistId, version); + } } } diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java index 9e510a68b..59f75e4ed 100644 --- a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java +++ b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java @@ -36,6 +36,7 @@ public class WordListMetadata { public final String mRemoteFilename; public final int mVersion; // version of this word list public final int mFlags; // Always 0 in this version, reserved for future use + public int mRetryCount; // The locale is matched against the locale requested by the client. The matching algorithm // is a standard locale matching with fallback; it is implemented in @@ -51,8 +52,9 @@ public class WordListMetadata { public WordListMetadata(final String id, final int type, final String description, final long lastUpdate, final long fileSize, - final String rawChecksum, final String checksum, final String localFilename, - final String remoteFilename, final int version, final int formatVersion, + final String rawChecksum, final String checksum, final int retryCount, + final String localFilename, final String remoteFilename, + final int version, final int formatVersion, final int flags, final String locale) { mId = id; mType = type; @@ -61,6 +63,7 @@ public class WordListMetadata { mFileSize = fileSize; mRawChecksum = rawChecksum; mChecksum = checksum; + mRetryCount = retryCount; mLocalFilename = localFilename; mRemoteFilename = remoteFilename; mVersion = version; @@ -82,6 +85,7 @@ public class WordListMetadata { final Long fileSize = values.getAsLong(MetadataDbHelper.FILESIZE_COLUMN); final String rawChecksum = values.getAsString(MetadataDbHelper.RAW_CHECKSUM_COLUMN); final String checksum = values.getAsString(MetadataDbHelper.CHECKSUM_COLUMN); + final int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN); final String localFilename = values.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); final String remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN); final Integer version = values.getAsInteger(MetadataDbHelper.VERSION_COLUMN); @@ -103,7 +107,8 @@ public class WordListMetadata { throw new IllegalArgumentException(); } return new WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum, - checksum, localFilename, remoteFilename, version, formatVersion, flags, locale); + checksum, retryCount, localFilename, remoteFilename, version, formatVersion, + flags, locale); } @Override @@ -116,6 +121,7 @@ public class WordListMetadata { sb.append("\nFileSize : ").append(mFileSize); sb.append("\nRawChecksum : ").append(mRawChecksum); sb.append("\nChecksum : ").append(mChecksum); + sb.append("\nRetryCount: ").append(mRetryCount); sb.append("\nLocalFilename : ").append(mLocalFilename); sb.append("\nRemoteFilename : ").append(mRemoteFilename); sb.append("\nVersion : ").append(mVersion); diff --git a/java/src/com/android/inputmethod/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"); } } } |