BinaryDictionaryGetter.java revision 0df78d46da1ef0d42196f3baa9d5f6df5932afb6
1cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard/* 2cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Copyright (C) 2011 The Android Open Source Project 3cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * 4cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * use this file except in compliance with the License. You may obtain a copy of 6cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * the License at 7cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * 8cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * http://www.apache.org/licenses/LICENSE-2.0 9cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * 10cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Unless required by applicable law or agreed to in writing, software 11cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * License for the specific language governing permissions and limitations under 14cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * the License. 15cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard */ 16cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard 17cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardpackage com.android.inputmethod.latin; 18cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard 19cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardimport android.content.Context; 2086e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalardimport android.content.SharedPreferences; 2186e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalardimport android.content.pm.PackageManager.NameNotFoundException; 22cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardimport android.content.res.AssetFileDescriptor; 23cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardimport android.util.Log; 24cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard 2528966734619251f78812f6a53f5efacbf5f77c49Jean Chalardimport java.io.File; 2608868624ede5eb4950972833f015d465408d3408Jean Chalardimport java.util.ArrayList; 270df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalardimport java.util.HashMap; 28cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardimport java.util.Locale; 29cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard 30cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard/** 31cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Helper class to get the address of a mmap'able dictionary file. 32cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard */ 33cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardclass BinaryDictionaryGetter { 34cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard 35cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard /** 36cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Used for Log actions from this class 37cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard */ 38cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard private static final String TAG = BinaryDictionaryGetter.class.getSimpleName(); 39cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard 4086e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard /** 4183207fb482b13bd2300008aa153080f0706fbd8dJean Chalard * Used to return empty lists 4283207fb482b13bd2300008aa153080f0706fbd8dJean Chalard */ 4383207fb482b13bd2300008aa153080f0706fbd8dJean Chalard private static final File[] EMPTY_FILE_ARRAY = new File[0]; 4483207fb482b13bd2300008aa153080f0706fbd8dJean Chalard 4583207fb482b13bd2300008aa153080f0706fbd8dJean Chalard /** 4686e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard * Name of the common preferences name to know which word list are on and which are off. 4786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard */ 4886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"; 4986e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard 500df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard // Name of the category for the main dictionary 510df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard private static final String MAIN_DICTIONARY_CATEGORY = "main"; 520df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard public static final String ID_CATEGORY_SEPARATOR = ":"; 530df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard 54cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard // Prevents this from being instantiated 55cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard private BinaryDictionaryGetter() {} 56cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard 57cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard /** 5886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard * Returns whether we may want to use this character as part of a file name. 5986e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard * 6086e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard * This basically only accepts ascii letters and numbers, and rejects everything else. 6186e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard */ 6286e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard private static boolean isFileNameCharacter(int codePoint) { 6386e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit 6486e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase 6586e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase 6686e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard return codePoint == '_'; // Underscore 6786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard } 6886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard 6986e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard /** 7028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * Escapes a string for any characters that may be suspicious for a file or directory name. 7128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * 7228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * Concretely this does a sort of URL-encoding except it will encode everything that's not 7328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which 7428966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * we cannot allow here) 7528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard */ 7628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard // TODO: create a unit test for this method 7786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard private static String replaceFileNameDangerousCharacters(final String name) { 7828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard // This assumes '%' is fully available as a non-separator, normal 7928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard // character in a file name. This is probably true for all file systems. 8028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard final StringBuilder sb = new StringBuilder(); 819242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard final int nameLength = name.length(); 829242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) { 8328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard final int codePoint = name.codePointAt(i); 8486e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard if (isFileNameCharacter(codePoint)) { 8528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard sb.appendCodePoint(codePoint); 8628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard } else { 8786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard // 6 digits - unicode is limited to 21 bits 8886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard sb.append(String.format((Locale)null, "%%%1$06x", codePoint)); 8986e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard } 9086e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard } 9186e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard return sb.toString(); 9286e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard } 9386e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard 9486e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard /** 9586e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard * Reverse escaping done by replaceFileNameDangerousCharacters. 9686e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard */ 9786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard private static String getWordListIdFromFileName(final String fname) { 9886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard final StringBuilder sb = new StringBuilder(); 999242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard final int fnameLength = fname.length(); 1009242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) { 10186e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard final int codePoint = fname.codePointAt(i); 10286e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard if ('%' != codePoint) { 10386e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard sb.appendCodePoint(codePoint); 10486e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard } else { 10586e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard final int encodedCodePoint = Integer.parseInt(fname.substring(i + 1, i + 7), 16); 10686e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard i += 6; 10786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard sb.appendCodePoint(encodedCodePoint); 10828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard } 10928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard } 11028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard return sb.toString(); 11128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard } 11228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard 11328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard /** 114de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * Helper method to get the top level cache directory. 115de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard */ 116de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard private static String getWordListCacheDirectory(final Context context) { 117de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard return context.getFilesDir() + File.separator + "dicts"; 118de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard } 119de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard 120de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard /** 12128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * Find out the cache directory associated with a specific locale. 12228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard */ 123de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard private static String getCacheDirectoryForLocale(final String locale, final Context context) { 124de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale); 125de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator 12628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard + relativeDirectoryName; 12728966734619251f78812f6a53f5efacbf5f77c49Jean Chalard final File directory = new File(absoluteDirectoryName); 12828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard if (!directory.exists()) { 12928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard if (!directory.mkdirs()) { 13028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard Log.e(TAG, "Could not create the directory for locale" + locale); 13128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard } 13228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard } 13328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard return absoluteDirectoryName; 13428966734619251f78812f6a53f5efacbf5f77c49Jean Chalard } 13528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard 13628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard /** 13728966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * Generates a file name for the id and locale passed as an argument. 13828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * 13928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * In the current implementation the file name returned will always be unique for 14028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * any id/locale pair, but please do not expect that the id can be the same for 14128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * different dictionaries with different locales. An id should be unique for any 14228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * dictionary. 14328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * The file name is pretty much an URL-encoded version of the id inside a directory 14428966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * named like the locale, except it will also escape characters that look dangerous 14528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * to some file systems. 14628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * @param id the id of the dictionary for which to get a file name 147de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * @param locale the locale for which to get the file name as a string 14828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * @param context the context to use for getting the directory 14928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard * @return the name of the file to be created 15028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard */ 151de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard public static String getCacheFileName(String id, String locale, Context context) { 15228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard final String fileName = replaceFileNameDangerousCharacters(id); 15328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard return getCacheDirectoryForLocale(locale, context) + File.separator + fileName; 15428966734619251f78812f6a53f5efacbf5f77c49Jean Chalard } 15528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard 15628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard /** 157cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Returns a file address from a resource, or null if it cannot be opened. 158cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard */ 159e150ef98569d61078e0f8c67ded8364a9c3d4a20Jean Chalard private static AssetFileAddress loadFallbackResource(final Context context, 16078ab80844b4f8e0369f4e86b2a02208197f9bd34Tadashi G. Takaoka final int fallbackResId) { 16178ab80844b4f8e0369f4e86b2a02208197f9bd34Tadashi G. Takaoka final AssetFileDescriptor afd = context.getResources().openRawResourceFd(fallbackResId); 162cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard if (afd == null) { 163cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard Log.e(TAG, "Found the resource but cannot read it. Is it compressed? resId=" 164cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard + fallbackResId); 165cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard return null; 166cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard } 167cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard return AssetFileAddress.makeFromFileNameAndOffset( 168cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength()); 169cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard } 170cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard 171c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard static private class DictPackSettings { 172c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard final SharedPreferences mDictPreferences; 173c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard public DictPackSettings(final Context context) { 174c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard Context dictPackContext = null; 175c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard try { 176c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard final String dictPackName = 177c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard context.getString(R.string.dictionary_pack_package_name); 178c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard dictPackContext = context.createPackageContext(dictPackName, 0); 179c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard } catch (NameNotFoundException e) { 180c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // The dictionary pack is not installed... 181c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // TODO: fallback on the built-in dict, see the TODO above 182c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard Log.e(TAG, "Could not find a dictionary pack"); 183c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard } 184c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard mDictPreferences = null == dictPackContext ? null 185c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard : dictPackContext.getSharedPreferences(COMMON_PREFERENCES_NAME, 186c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS); 187c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard } 188c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard public boolean isWordListActive(final String dictId) { 189c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard if (null == mDictPreferences) { 190c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // If we don't have preferences it basically means we can't find the dictionary 191c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // pack - either it's not installed, or it's disabled, or there is some strange 192c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // bug. Either way, a word list with no settings should be on by default: default 193c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // dictionaries in LatinIME are on if there is no settings at all, and if for some 194c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // reason some dictionaries have been installed BUT the dictionary pack can't be 195c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // found anymore it's safer to actually supply installed dictionaries. 196c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard return true; 197c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard } else { 198c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // The default is true here for the same reasons as above. We got the dictionary 199c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // pack but if we don't have any settings for it it means the user has never been 200c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard // to the settings yet. So by default, the main dictionaries should be on. 201c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard return mDictPreferences.getBoolean(dictId, true); 202c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard } 203c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard } 204c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard } 205c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard 206cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard /** 207de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * Helper method to the list of cache directories, one for each distinct locale. 208de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard */ 209de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard private static File[] getCachedDirectoryList(final Context context) { 210de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard return new File(getWordListCacheDirectory(context)).listFiles(); 211de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard } 212de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard 213de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard /** 2140df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * Returns the category for a given file name. 2150df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * 2160df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * This parses the file name, extracts the category, and returns it. See 2170df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}. 2180df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * @return The category as a string or null if it can't be found in the file name. 2190df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard */ 2200df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard private static String getCategoryFromFileName(final String fileName) { 2210df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final String id = getWordListIdFromFileName(fileName); 2220df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final String[] idArray = id.split(ID_CATEGORY_SEPARATOR); 2230df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard if (2 != idArray.length) return null; 2240df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard return idArray[0]; 2250df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard } 2260df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard 2270df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard /** 2280df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * Utility class for the {@link #getCachedWordLists} method 2290df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard */ 2300df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard private static class FileAndMatchLevel { 2310df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final File mFile; 2320df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final int mMatchLevel; 2330df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard public FileAndMatchLevel(final File file, final int matchLevel) { 2340df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard mFile = file; 2350df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard mMatchLevel = matchLevel; 2360df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard } 2370df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard } 2380df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard 2390df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard /** 2400df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * Returns the list of cached files for a specific locale, one for each category. 2410df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * 2420df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * This will return exactly one file for each word list category that matches 2430df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * the passed locale. If several files match the locale for any given category, 2440df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * this returns the file with the closest match to the locale. For example, if 2450df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * the passed word list is en_US, and for a category we have an en and an en_US 2460df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * word list available, we'll return only the en_US one. 2470df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard * Thus, the list will contain as many files as there are categories. 24808868624ede5eb4950972833f015d465408d3408Jean Chalard * 249de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * @param locale the locale to find the dictionary files for, as a string. 25008868624ede5eb4950972833f015d465408d3408Jean Chalard * @param context the context on which to open the files upon. 25183207fb482b13bd2300008aa153080f0706fbd8dJean Chalard * @return an array of binary dictionary files, which may be empty but may not be null. 25208868624ede5eb4950972833f015d465408d3408Jean Chalard */ 253de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard private static File[] getCachedWordLists(final String locale, 25408868624ede5eb4950972833f015d465408d3408Jean Chalard final Context context) { 255de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard final File[] directoryList = getCachedDirectoryList(context); 256de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard if (null == directoryList) return EMPTY_FILE_ARRAY; 2570df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final HashMap<String, FileAndMatchLevel> cacheFiles = 2580df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard new HashMap<String, FileAndMatchLevel>(); 259de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard for (File directory : directoryList) { 260de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard if (!directory.isDirectory()) continue; 261de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard final String dirLocale = getWordListIdFromFileName(directory.getName()); 2620df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale); 2630df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard if (LocaleUtils.isMatch(matchLevel)) { 264de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard final File[] wordLists = directory.listFiles(); 265de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard if (null != wordLists) { 266de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard for (File wordList : wordLists) { 2670df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final String category = getCategoryFromFileName(wordList.getName()); 2680df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final FileAndMatchLevel currentBestMatch = cacheFiles.get(category); 2690df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard if (null == currentBestMatch || currentBestMatch.mMatchLevel < matchLevel) { 2700df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard cacheFiles.put(category, new FileAndMatchLevel(wordList, matchLevel)); 2710df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard } 272de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard } 273de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard } 274de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard } 275de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard } 276de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard if (cacheFiles.isEmpty()) return EMPTY_FILE_ARRAY; 2770df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final File[] result = new File[cacheFiles.size()]; 2780df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard int index = 0; 2790df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard for (final FileAndMatchLevel entry : cacheFiles.values()) { 2800df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard result[index++] = entry.mFile; 2810df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard } 2820df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard return result; 28308868624ede5eb4950972833f015d465408d3408Jean Chalard } 28408868624ede5eb4950972833f015d465408d3408Jean Chalard 28508868624ede5eb4950972833f015d465408d3408Jean Chalard /** 286de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * Returns the id associated with the main word list for a specified locale. 287de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * 288de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * Word lists stored in Android Keyboard's resources are referred to as the "main" 289de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * word lists. Since they can be updated like any other list, we need to assign a 290de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * unique ID to them. This ID is just the name of the language (locale-wise) they 291de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard * are for, and this method returns this ID. 292ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard */ 293ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard private static String getMainDictId(final Locale locale) { 294de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard // This works because we don't include by default different dictionaries for 295de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard // different countries. This actually needs to return the id that we would 296de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard // like to use for word lists included in resources, and the following is okay. 2970df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard return MAIN_DICTIONARY_CATEGORY + ID_CATEGORY_SEPARATOR + locale.getLanguage().toString(); 2980df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard } 2990df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard 3000df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard private static boolean isMainWordListId(final String id) { 3010df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard final String[] idArray = id.split(ID_CATEGORY_SEPARATOR); 3020df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard if (2 != idArray.length) return false; 3030df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard return MAIN_DICTIONARY_CATEGORY.equals(idArray[0]); 304ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard } 305ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard 306ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard /** 307d8f52a4f18d22aa150846b01017410ce70bbad6fJean Chalard * Returns a list of file addresses for a given locale, trying relevant methods in order. 308cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * 309d8f52a4f18d22aa150846b01017410ce70bbad6fJean Chalard * Tries to get binary dictionaries from various sources, in order: 310d8f52a4f18d22aa150846b01017410ce70bbad6fJean Chalard * - Uses a content provider to get a public dictionary set, as per the protocol described 311cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * in BinaryDictionaryFileDumper. 312cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * If that fails: 313e6269759d642eac0a03ae6942acb5cd556e7ff46Jean Chalard * - Gets a file name from the built-in dictionary for this locale, if any. 314cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * If that fails: 315cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * - Returns null. 316660776e09b9a3b321074a94721d901a035ca1b9fKen Wakasa * @return The list of addresses of valid dictionary files, or null. 317cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard */ 318660776e09b9a3b321074a94721d901a035ca1b9fKen Wakasa public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale, 319e6269759d642eac0a03ae6942acb5cd556e7ff46Jean Chalard final Context context) { 32080e0bf04292867ddc769aca75ebaee817b95a941Jean Chalard 321cec8552b18fd74517512a43a8d75f64e64bd12c3Jean Chalard final boolean hasDefaultWordList = DictionaryFactory.isDictionaryAvailable(context, locale); 3227b1f74bb9ddae952f4da6c8d9bbb0057984b0988Jean Chalard // cacheWordListsFromContentProvider returns the list of files it copied to local 32380e0bf04292867ddc769aca75ebaee817b95a941Jean Chalard // storage, but we don't really care about what was copied NOW: what we want is the 32480e0bf04292867ddc769aca75ebaee817b95a941Jean Chalard // list of everything we ever cached, so we ignore the return value. 325cec8552b18fd74517512a43a8d75f64e64bd12c3Jean Chalard BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context, 326cec8552b18fd74517512a43a8d75f64e64bd12c3Jean Chalard hasDefaultWordList); 327de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard final File[] cachedWordLists = getCachedWordLists(locale.toString(), context); 328ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard final String mainDictId = getMainDictId(locale); 32983207fb482b13bd2300008aa153080f0706fbd8dJean Chalard final DictPackSettings dictPackSettings = new DictPackSettings(context); 33083207fb482b13bd2300008aa153080f0706fbd8dJean Chalard 331ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard boolean foundMainDict = false; 33283207fb482b13bd2300008aa153080f0706fbd8dJean Chalard final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>(); 3337b1f74bb9ddae952f4da6c8d9bbb0057984b0988Jean Chalard // cachedWordLists may not be null, see doc for getCachedDictionaryList 3347b1f74bb9ddae952f4da6c8d9bbb0057984b0988Jean Chalard for (final File f : cachedWordLists) { 33583207fb482b13bd2300008aa153080f0706fbd8dJean Chalard final String wordListId = getWordListIdFromFileName(f.getName()); 3360df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard if (isMainWordListId(wordListId)) { 337ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard foundMainDict = true; 338ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard } 33983207fb482b13bd2300008aa153080f0706fbd8dJean Chalard if (!dictPackSettings.isWordListActive(wordListId)) continue; 34083207fb482b13bd2300008aa153080f0706fbd8dJean Chalard if (f.canRead()) { 34183207fb482b13bd2300008aa153080f0706fbd8dJean Chalard fileList.add(AssetFileAddress.makeFromFileName(f.getPath())); 34283207fb482b13bd2300008aa153080f0706fbd8dJean Chalard } else { 34383207fb482b13bd2300008aa153080f0706fbd8dJean Chalard Log.e(TAG, "Found a cached dictionary file but cannot read it"); 34483207fb482b13bd2300008aa153080f0706fbd8dJean Chalard } 34583207fb482b13bd2300008aa153080f0706fbd8dJean Chalard } 34683207fb482b13bd2300008aa153080f0706fbd8dJean Chalard 347ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) { 348e6269759d642eac0a03ae6942acb5cd556e7ff46Jean Chalard final int fallbackResId = 349e6269759d642eac0a03ae6942acb5cd556e7ff46Jean Chalard DictionaryFactory.getMainDictionaryResourceId(context.getResources(), locale); 35078ab80844b4f8e0369f4e86b2a02208197f9bd34Tadashi G. Takaoka final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId); 351ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard if (null != fallbackAsset) { 352ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard fileList.add(fallbackAsset); 353ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard } 354cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard } 35580e0bf04292867ddc769aca75ebaee817b95a941Jean Chalard 356ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard return fileList; 357cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard } 358cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard} 359