From a0d9c82921022347e44d416bb57810331e35e446 Mon Sep 17 00:00:00 2001 From: Mohammadinamul Sheik Date: Mon, 4 May 2015 11:57:31 -0700 Subject: [PATCH] Make the DictionaryService stage the downloaded files Bug: 20641948 Change-Id: I6639c995b12c033bc30241cd219201dd483ee516 --- .../inputmethod/latin/common/FileUtils.java | 14 +++++ .../dictionarypack/ActionBatch.java | 10 ++++ .../dictionarypack/UpdateHandler.java | 2 + .../latin/BinaryDictionaryFileDumper.java | 39 ++++++++---- .../latin/BinaryDictionaryGetter.java | 49 ++++----------- .../latin/utils/DictionaryInfoUtils.java | 60 ++++++++++++++++++- 6 files changed, 122 insertions(+), 52 deletions(-) diff --git a/common/src/com/android/inputmethod/latin/common/FileUtils.java b/common/src/com/android/inputmethod/latin/common/FileUtils.java index 676845842..16732660c 100644 --- a/common/src/com/android/inputmethod/latin/common/FileUtils.java +++ b/common/src/com/android/inputmethod/latin/common/FileUtils.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin.common; +import android.util.Log; + import java.io.File; import java.io.FilenameFilter; @@ -23,6 +25,8 @@ import java.io.FilenameFilter; * A simple class to help with removing directories recursively. */ public class FileUtils { + private static final String TAG = "FileUtils"; + public static boolean deleteRecursively(final File path) { if (path.isDirectory()) { final File[] files = path.listFiles(); @@ -51,4 +55,14 @@ public class FileUtils { } return hasDeletedAllFiles; } + + public static boolean renameTo(final File fromFile, final File toFile) { + toFile.delete(); + final boolean success = fromFile.renameTo(toFile); + if (!success) { + Log.e(TAG, String.format("Failed to rename from %s to %s.", + fromFile.getAbsoluteFile(), toFile.getAbsoluteFile())); + } + return success; + } } diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java index 09f8032cc..ee5106b5a 100644 --- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java +++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java @@ -26,7 +26,9 @@ import android.text.TextUtils; import android.util.Log; import com.android.inputmethod.compat.DownloadManagerCompatUtils; +import com.android.inputmethod.latin.BinaryDictionaryFileDumper; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.common.LocaleUtils; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.DebugLogUtils; @@ -210,9 +212,17 @@ public final class ActionBatch { + " for an InstallAfterDownload action. Bailing out."); return; } + DebugLogUtils.l("Setting word list as installed"); final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues); + + // Install the downloaded file by un-compressing and moving it to the staging + // directory. Ideally, we should do this before updating the DB, but the + // installDictToStagingFromContentProvider() relies on the db being updated. + final String localeString = mWordListValues.getAsString(MetadataDbHelper.LOCALE_COLUMN); + BinaryDictionaryFileDumper.installDictToStagingFromContentProvider( + LocaleUtils.constructLocaleFromString(localeString), context, false); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index e61547a9d..6570171a6 100644 --- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java @@ -592,6 +592,8 @@ public final class UpdateHandler { * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data. */ private static void signalNewDictionaryState(final Context context) { + // TODO: Also provide the locale of the updated dictionary so that the LatinIme + // does not have to reset if it is a different locale. final Intent newDictBroadcast = new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); context.sendBroadcast(newDictBroadcast); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index bc62f3ae3..4b242c5f5 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -29,6 +29,7 @@ import android.util.Log; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.dictionarypack.MD5Calculator; +import com.android.inputmethod.latin.common.FileUtils; import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils.DictionaryInfo; @@ -220,11 +221,11 @@ public final class BinaryDictionaryFileDumper { } /** - * Caches a word list the id of which is passed as an argument. This will write the file + * Stages a word list the id of which is passed as an argument. This will write the file * to the cache file name designated by its id and locale, overwriting it if already present * and creating it (and its containing directory) if necessary. */ - private static void cacheWordList(final String wordlistId, final String locale, + private static void installWordListToStaging(final String wordlistId, final String locale, final String rawChecksum, final ContentProviderClient providerClient, final Context context) { final int COMPRESSED_CRYPTED_COMPRESSED = 0; @@ -246,7 +247,7 @@ public final class BinaryDictionaryFileDumper { return; } final String finalFileName = - DictionaryInfoUtils.getCacheFileName(wordlistId, locale, context); + DictionaryInfoUtils.getStagingFileName(wordlistId, locale, context); String tempFileName; try { tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context); @@ -320,23 +321,21 @@ public final class BinaryDictionaryFileDumper { } } + // move the output file to the final staging file. final File finalFile = new File(finalFileName); - finalFile.delete(); - if (!outputFile.renameTo(finalFile)) { - throw new IOException("Can't move the file to its final name"); - } + FileUtils.renameTo(outputFile, finalFile); + wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, QUERY_PARAMETER_SUCCESS); if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { Log.e(TAG, "Could not have the dictionary pack delete a word list"); } - BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile); - Log.e(TAG, "Successfully copied file for wordlist ID " + wordlistId); + Log.d(TAG, "Successfully copied file for wordlist ID " + wordlistId); // Success! Close files (through the finally{} clause) and return. return; } catch (Exception e) { if (DEBUG) { - Log.i(TAG, "Can't open word list in mode " + mode, e); + Log.e(TAG, "Can't open word list in mode " + mode, e); } if (null != outputFile) { // This may or may not fail. The file may not have been created if the @@ -403,7 +402,7 @@ public final class BinaryDictionaryFileDumper { } /** - * Queries a content provider for word list data for some locale and cache the returned files + * Queries a content provider for word list data for some locale and stage the returned files * * This will query a content provider for word list data for a given locale, and copy the * files locally so that they can be mmap'ed. This may overwrite previously cached word lists @@ -411,7 +410,7 @@ public final class BinaryDictionaryFileDumper { * @throw FileNotFoundException if the provider returns non-existent data. * @throw IOException if the provider-returned data could not be read. */ - public static void cacheWordListsFromContentProvider(final Locale locale, + public static void installDictToStagingFromContentProvider(final Locale locale, final Context context, final boolean hasDefaultWordList) { final ContentProviderClient providerClient; try { @@ -429,13 +428,27 @@ public final class BinaryDictionaryFileDumper { final List idList = getWordListWordListInfos(locale, context, hasDefaultWordList); for (WordListInfo id : idList) { - cacheWordList(id.mId, id.mLocale, id.mRawChecksum, providerClient, context); + installWordListToStaging(id.mId, id.mLocale, id.mRawChecksum, providerClient, + context); } } finally { providerClient.release(); } } + /** + * Downloads the dictionary if it was never requested/used. + * + * @param locale locale to download + * @param context the context for resources and providers. + * @param hasDefaultWordList whether the default wordlist exists in the resources. + */ + public static void downloadDictIfNeverRequested(final Locale locale, + final Context context, final boolean hasDefaultWordList) { + Log.d("inamul_tag", "BinaryDictionaryFileDumper.downloadDictIfNeverRequested()"); + getWordListWordListInfos(locale, context, hasDefaultWordList); + } + /** * Copies the data in an input stream to a target file if the magic number matches. * diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 5f2a112ba..60016371b 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -195,39 +195,6 @@ final public class BinaryDictionaryGetter { return result; } - /** - * Remove all files with the passed id, except the passed file. - * - * If a dictionary with a given ID has a metadata change that causes it to change - * path, we need to remove the old version. The only way to do this is to check all - * installed files for a matching ID in a different directory. - */ - public static void removeFilesWithIdExcept(final Context context, final String id, - final File fileToKeep) { - try { - final File canonicalFileToKeep = fileToKeep.getCanonicalFile(); - final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context); - if (null == directoryList) return; - for (File directory : directoryList) { - // There is one directory per locale. See #getCachedDirectoryList - if (!directory.isDirectory()) continue; - final File[] wordLists = directory.listFiles(); - if (null == wordLists) continue; - for (File wordList : wordLists) { - final String fileId = - DictionaryInfoUtils.getWordListIdFromFileName(wordList.getName()); - if (fileId.equals(id)) { - if (!canonicalFileToKeep.equals(wordList.getCanonicalFile())) { - wordList.delete(); - } - } - } - } - } catch (java.io.IOException e) { - Log.e(TAG, "IOException trying to cleanup files", e); - } - } - // ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since // those do not include whitelist entries, the new code with an old version of the dictionary // would lose whitelist functionality. @@ -274,12 +241,18 @@ final public class BinaryDictionaryGetter { */ public static ArrayList getDictionaryFiles(final Locale locale, final Context context, boolean notifyDictionaryPackForUpdates) { - - final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( - context, locale); if (notifyDictionaryPackForUpdates) { - BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context, - hasDefaultWordList); + final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( + context, locale); + // It makes sure that the first time keyboard comes up and the dictionaries are reset, + // the DB is populated with the appropriate values for each locale. Helps in downloading + // the dictionaries when the user enables and switches new languages before the + // DictionaryService runs. + BinaryDictionaryFileDumper.downloadDictIfNeverRequested( + locale, context, hasDefaultWordList); + + // Move a staging files to the cache ddirectories if any. + DictionaryInfoUtils.moveStagingFilesIfExists(context); } final File[] cachedWordLists = getCachedWordLists(locale.toString(), context); final String mainDictId = DictionaryInfoUtils.getMainDictId(locale); diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index cfa977a46..096a545e5 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -30,6 +30,7 @@ import com.android.inputmethod.latin.AssetFileAddress; import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; +import com.android.inputmethod.latin.common.FileUtils; import com.android.inputmethod.latin.common.LocaleUtils; import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.makedict.DictionaryHeader; @@ -152,6 +153,13 @@ public class DictionaryInfoUtils { return context.getFilesDir() + File.separator + "dicts"; } + /** + * Helper method to get the top level cache directory. + */ + public static String getWordListStagingDirectory(final Context context) { + return context.getFilesDir() + File.separator + "staging"; + } + /** * Helper method to get the top level temp directory. */ @@ -188,6 +196,10 @@ public class DictionaryInfoUtils { return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles(); } + public static File[] getStagingDirectoryList(final Context context) { + return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles(); + } + @Nullable public static File[] getUnusedDictionaryList(final Context context) { return context.getFilesDir().listFiles(new FilenameFilter() { @@ -221,7 +233,7 @@ public class DictionaryInfoUtils { /** * Find out the cache directory associated with a specific locale. */ - private static String getCacheDirectoryForLocale(final String locale, final Context context) { + public static String getCacheDirectoryForLocale(final String locale, final Context context) { final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale); final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator + relativeDirectoryName; @@ -254,6 +266,52 @@ public class DictionaryInfoUtils { return getCacheDirectoryForLocale(locale, context) + File.separator + fileName; } + public static String getStagingFileName(String id, String locale, Context context) { + final String stagingDirectory = getWordListStagingDirectory(context); + // create the directory if it does not exist. + final File directory = new File(stagingDirectory); + if (!directory.exists()) { + if (!directory.mkdirs()) { + Log.e(TAG, "Could not create the staging directory."); + } + } + // e.g. id="main:en_in", locale ="en_IN" + final String fileName = replaceFileNameDangerousCharacters( + locale + TEMP_DICT_FILE_SUB + id); + return stagingDirectory + File.separator + fileName; + } + + public static void moveStagingFilesIfExists(Context context) { + final File[] stagingFiles = DictionaryInfoUtils.getStagingDirectoryList(context); + if (stagingFiles != null && stagingFiles.length > 0) { + for (final File stagingFile : stagingFiles) { + final String fileName = stagingFile.getName(); + final int index = fileName.indexOf(TEMP_DICT_FILE_SUB); + if (index == -1) { + // This should never happen. + Log.e(TAG, "Staging file does not have ___ substring."); + continue; + } + final String[] localeAndFileId = fileName.split(TEMP_DICT_FILE_SUB); + if (localeAndFileId.length != 2) { + Log.e(TAG, String.format("malformed staging file %s. Deleting.", + stagingFile.getAbsoluteFile())); + stagingFile.delete(); + continue; + } + + final String locale = localeAndFileId[0]; + // already escaped while moving to staging. + final String fileId = localeAndFileId[1]; + final String cacheDirectoryForLocale = getCacheDirectoryForLocale(locale, context); + final String cacheFilename = cacheDirectoryForLocale + File.separator + fileId; + final File cacheFile = new File(cacheFilename); + // move the staging file to cache file. + FileUtils.renameTo(stagingFile, cacheFile); + } + } + } + public static boolean isMainWordListId(final String id) { final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); // An id is supposed to be in format category:locale, so splitting on the separator