Make the DictionaryService stage the downloaded files

Bug: 20641948
Change-Id: I6639c995b12c033bc30241cd219201dd483ee516
main
Mohammadinamul Sheik 2015-05-04 11:57:31 -07:00
parent 459b4f353e
commit a0d9c82921
6 changed files with 122 additions and 52 deletions

View File

@ -16,6 +16,8 @@
package com.android.inputmethod.latin.common; package com.android.inputmethod.latin.common;
import android.util.Log;
import java.io.File; import java.io.File;
import java.io.FilenameFilter; import java.io.FilenameFilter;
@ -23,6 +25,8 @@ import java.io.FilenameFilter;
* A simple class to help with removing directories recursively. * A simple class to help with removing directories recursively.
*/ */
public class FileUtils { public class FileUtils {
private static final String TAG = "FileUtils";
public static boolean deleteRecursively(final File path) { public static boolean deleteRecursively(final File path) {
if (path.isDirectory()) { if (path.isDirectory()) {
final File[] files = path.listFiles(); final File[] files = path.listFiles();
@ -51,4 +55,14 @@ public class FileUtils {
} }
return hasDeletedAllFiles; 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;
}
} }

View File

@ -26,7 +26,9 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.android.inputmethod.compat.DownloadManagerCompatUtils; import com.android.inputmethod.compat.DownloadManagerCompatUtils;
import com.android.inputmethod.latin.BinaryDictionaryFileDumper;
import com.android.inputmethod.latin.R; 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.ApplicationUtils;
import com.android.inputmethod.latin.utils.DebugLogUtils; import com.android.inputmethod.latin.utils.DebugLogUtils;
@ -210,9 +212,17 @@ public final class ActionBatch {
+ " for an InstallAfterDownload action. Bailing out."); + " for an InstallAfterDownload action. Bailing out.");
return; return;
} }
DebugLogUtils.l("Setting word list as installed"); DebugLogUtils.l("Setting word list as installed");
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues); 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);
} }
} }

View File

@ -592,6 +592,8 @@ public final class UpdateHandler {
* Warn Android Keyboard that the state of dictionaries changed and it should refresh its data. * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data.
*/ */
private static void signalNewDictionaryState(final Context context) { 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 = final Intent newDictBroadcast =
new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
context.sendBroadcast(newDictBroadcast); context.sendBroadcast(newDictBroadcast);

View File

@ -29,6 +29,7 @@ import android.util.Log;
import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
import com.android.inputmethod.dictionarypack.MD5Calculator; 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.define.DecoderSpecificConstants;
import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
import com.android.inputmethod.latin.utils.DictionaryInfoUtils.DictionaryInfo; 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 * 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. * 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 String rawChecksum, final ContentProviderClient providerClient,
final Context context) { final Context context) {
final int COMPRESSED_CRYPTED_COMPRESSED = 0; final int COMPRESSED_CRYPTED_COMPRESSED = 0;
@ -246,7 +247,7 @@ public final class BinaryDictionaryFileDumper {
return; return;
} }
final String finalFileName = final String finalFileName =
DictionaryInfoUtils.getCacheFileName(wordlistId, locale, context); DictionaryInfoUtils.getStagingFileName(wordlistId, locale, context);
String tempFileName; String tempFileName;
try { try {
tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context); 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); final File finalFile = new File(finalFileName);
finalFile.delete(); FileUtils.renameTo(outputFile, finalFile);
if (!outputFile.renameTo(finalFile)) {
throw new IOException("Can't move the file to its final name");
}
wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
QUERY_PARAMETER_SUCCESS); QUERY_PARAMETER_SUCCESS);
if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) {
Log.e(TAG, "Could not have the dictionary pack delete a word list"); Log.e(TAG, "Could not have the dictionary pack delete a word list");
} }
BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile); Log.d(TAG, "Successfully copied file for wordlist ID " + wordlistId);
Log.e(TAG, "Successfully copied file for wordlist ID " + wordlistId);
// Success! Close files (through the finally{} clause) and return. // Success! Close files (through the finally{} clause) and return.
return; return;
} catch (Exception e) { } catch (Exception e) {
if (DEBUG) { 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) { if (null != outputFile) {
// This may or may not fail. The file may not have been created if the // 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 * 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 * 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 FileNotFoundException if the provider returns non-existent data.
* @throw IOException if the provider-returned data could not be read. * @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 Context context, final boolean hasDefaultWordList) {
final ContentProviderClient providerClient; final ContentProviderClient providerClient;
try { try {
@ -429,13 +428,27 @@ public final class BinaryDictionaryFileDumper {
final List<WordListInfo> idList = getWordListWordListInfos(locale, context, final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
hasDefaultWordList); hasDefaultWordList);
for (WordListInfo id : idList) { for (WordListInfo id : idList) {
cacheWordList(id.mId, id.mLocale, id.mRawChecksum, providerClient, context); installWordListToStaging(id.mId, id.mLocale, id.mRawChecksum, providerClient,
context);
} }
} finally { } finally {
providerClient.release(); 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. * Copies the data in an input stream to a target file if the magic number matches.
* *

View File

@ -195,39 +195,6 @@ final public class BinaryDictionaryGetter {
return result; 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 // ## 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 // those do not include whitelist entries, the new code with an old version of the dictionary
// would lose whitelist functionality. // would lose whitelist functionality.
@ -274,12 +241,18 @@ final public class BinaryDictionaryGetter {
*/ */
public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale, public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
final Context context, boolean notifyDictionaryPackForUpdates) { final Context context, boolean notifyDictionaryPackForUpdates) {
final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable(
context, locale);
if (notifyDictionaryPackForUpdates) { if (notifyDictionaryPackForUpdates) {
BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context, final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable(
hasDefaultWordList); 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 File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale); final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);

View File

@ -30,6 +30,7 @@ import com.android.inputmethod.latin.AssetFileAddress;
import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.BinaryDictionaryGetter;
import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputMethodManager; 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.common.LocaleUtils;
import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.define.DecoderSpecificConstants;
import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.DictionaryHeader;
@ -152,6 +153,13 @@ public class DictionaryInfoUtils {
return context.getFilesDir() + File.separator + "dicts"; 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. * Helper method to get the top level temp directory.
*/ */
@ -188,6 +196,10 @@ public class DictionaryInfoUtils {
return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles(); return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
} }
public static File[] getStagingDirectoryList(final Context context) {
return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles();
}
@Nullable @Nullable
public static File[] getUnusedDictionaryList(final Context context) { public static File[] getUnusedDictionaryList(final Context context) {
return context.getFilesDir().listFiles(new FilenameFilter() { return context.getFilesDir().listFiles(new FilenameFilter() {
@ -221,7 +233,7 @@ public class DictionaryInfoUtils {
/** /**
* Find out the cache directory associated with a specific locale. * 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 relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
+ relativeDirectoryName; + relativeDirectoryName;
@ -254,6 +266,52 @@ public class DictionaryInfoUtils {
return getCacheDirectoryForLocale(locale, context) + File.separator + fileName; 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) { public static boolean isMainWordListId(final String id) {
final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
// An id is supposed to be in format category:locale, so splitting on the separator // An id is supposed to be in format category:locale, so splitting on the separator