1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.latin; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.content.res.AssetFileDescriptor; 22 import android.util.Log; 23 24 import com.android.inputmethod.latin.common.LocaleUtils; 25 import com.android.inputmethod.latin.define.DecoderSpecificConstants; 26 import com.android.inputmethod.latin.makedict.DictionaryHeader; 27 import com.android.inputmethod.latin.makedict.UnsupportedFormatException; 28 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; 29 import com.android.inputmethod.latin.utils.DictionaryInfoUtils; 30 31 import java.io.File; 32 import java.io.IOException; 33 import java.nio.BufferUnderflowException; 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.Locale; 37 38 /** 39 * Helper class to get the address of a mmap'able dictionary file. 40 */ 41 final public class BinaryDictionaryGetter { 42 43 /** 44 * Used for Log actions from this class 45 */ 46 private static final String TAG = BinaryDictionaryGetter.class.getSimpleName(); 47 48 /** 49 * Used to return empty lists 50 */ 51 private static final File[] EMPTY_FILE_ARRAY = new File[0]; 52 53 /** 54 * Name of the common preferences name to know which word list are on and which are off. 55 */ 56 private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"; 57 58 private static final boolean SHOULD_USE_DICT_VERSION = 59 DecoderSpecificConstants.SHOULD_USE_DICT_VERSION; 60 61 // Name of the category for the main dictionary 62 public static final String MAIN_DICTIONARY_CATEGORY = "main"; 63 public static final String ID_CATEGORY_SEPARATOR = ":"; 64 65 // The key considered to read the version attribute in a dictionary file. 66 private static String VERSION_KEY = "version"; 67 68 // Prevents this from being instantiated BinaryDictionaryGetter()69 private BinaryDictionaryGetter() {} 70 71 /** 72 * Generates a unique temporary file name in the app cache directory. 73 */ getTempFileName(final String id, final Context context)74 public static String getTempFileName(final String id, final Context context) 75 throws IOException { 76 final String safeId = DictionaryInfoUtils.replaceFileNameDangerousCharacters(id); 77 final File directory = new File(DictionaryInfoUtils.getWordListTempDirectory(context)); 78 if (!directory.exists()) { 79 if (!directory.mkdirs()) { 80 Log.e(TAG, "Could not create the temporary directory"); 81 } 82 } 83 // If the first argument is less than three chars, createTempFile throws a 84 // RuntimeException. We don't really care about what name we get, so just 85 // put a three-chars prefix makes us safe. 86 return File.createTempFile("xxx" + safeId, null, directory).getAbsolutePath(); 87 } 88 89 /** 90 * Returns a file address from a resource, or null if it cannot be opened. 91 */ loadFallbackResource(final Context context, final int fallbackResId)92 public static AssetFileAddress loadFallbackResource(final Context context, 93 final int fallbackResId) { 94 AssetFileDescriptor afd = null; 95 try { 96 afd = context.getResources().openRawResourceFd(fallbackResId); 97 } catch (RuntimeException e) { 98 Log.e(TAG, "Resource not found: " + fallbackResId); 99 return null; 100 } 101 if (afd == null) { 102 Log.e(TAG, "Resource cannot be opened: " + fallbackResId); 103 return null; 104 } 105 try { 106 return AssetFileAddress.makeFromFileNameAndOffset( 107 context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength()); 108 } finally { 109 try { 110 afd.close(); 111 } catch (IOException ignored) { 112 } 113 } 114 } 115 116 private static final class DictPackSettings { 117 final SharedPreferences mDictPreferences; DictPackSettings(final Context context)118 public DictPackSettings(final Context context) { 119 mDictPreferences = null == context ? null 120 : context.getSharedPreferences(COMMON_PREFERENCES_NAME, 121 Context.MODE_MULTI_PROCESS); 122 } isWordListActive(final String dictId)123 public boolean isWordListActive(final String dictId) { 124 if (null == mDictPreferences) { 125 // If we don't have preferences it basically means we can't find the dictionary 126 // pack - either it's not installed, or it's disabled, or there is some strange 127 // bug. Either way, a word list with no settings should be on by default: default 128 // dictionaries in LatinIME are on if there is no settings at all, and if for some 129 // reason some dictionaries have been installed BUT the dictionary pack can't be 130 // found anymore it's safer to actually supply installed dictionaries. 131 return true; 132 } 133 // The default is true here for the same reasons as above. We got the dictionary 134 // pack but if we don't have any settings for it it means the user has never been 135 // to the settings yet. So by default, the main dictionaries should be on. 136 return mDictPreferences.getBoolean(dictId, true); 137 } 138 } 139 140 /** 141 * Utility class for the {@link #getCachedWordLists} method 142 */ 143 private static final class FileAndMatchLevel { 144 final File mFile; 145 final int mMatchLevel; FileAndMatchLevel(final File file, final int matchLevel)146 public FileAndMatchLevel(final File file, final int matchLevel) { 147 mFile = file; 148 mMatchLevel = matchLevel; 149 } 150 } 151 152 /** 153 * Returns the list of cached files for a specific locale, one for each category. 154 * 155 * This will return exactly one file for each word list category that matches 156 * the passed locale. If several files match the locale for any given category, 157 * this returns the file with the closest match to the locale. For example, if 158 * the passed word list is en_US, and for a category we have an en and an en_US 159 * word list available, we'll return only the en_US one. 160 * Thus, the list will contain as many files as there are categories. 161 * 162 * @param locale the locale to find the dictionary files for, as a string. 163 * @param context the context on which to open the files upon. 164 * @return an array of binary dictionary files, which may be empty but may not be null. 165 */ getCachedWordLists(final String locale, final Context context)166 public static File[] getCachedWordLists(final String locale, final Context context) { 167 final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context); 168 if (null == directoryList) return EMPTY_FILE_ARRAY; 169 final HashMap<String, FileAndMatchLevel> cacheFiles = new HashMap<>(); 170 for (File directory : directoryList) { 171 if (!directory.isDirectory()) continue; 172 final String dirLocale = 173 DictionaryInfoUtils.getWordListIdFromFileName(directory.getName()); 174 final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale); 175 if (LocaleUtils.isMatch(matchLevel)) { 176 final File[] wordLists = directory.listFiles(); 177 if (null != wordLists) { 178 for (File wordList : wordLists) { 179 final String category = 180 DictionaryInfoUtils.getCategoryFromFileName(wordList.getName()); 181 final FileAndMatchLevel currentBestMatch = cacheFiles.get(category); 182 if (null == currentBestMatch || currentBestMatch.mMatchLevel < matchLevel) { 183 cacheFiles.put(category, new FileAndMatchLevel(wordList, matchLevel)); 184 } 185 } 186 } 187 } 188 } 189 if (cacheFiles.isEmpty()) return EMPTY_FILE_ARRAY; 190 final File[] result = new File[cacheFiles.size()]; 191 int index = 0; 192 for (final FileAndMatchLevel entry : cacheFiles.values()) { 193 result[index++] = entry.mFile; 194 } 195 return result; 196 } 197 198 // ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since 199 // those do not include whitelist entries, the new code with an old version of the dictionary 200 // would lose whitelist functionality. hackCanUseDictionaryFile(final File file)201 private static boolean hackCanUseDictionaryFile(final File file) { 202 if (!SHOULD_USE_DICT_VERSION) { 203 return true; 204 } 205 206 try { 207 // Read the version of the file 208 final DictionaryHeader header = BinaryDictionaryUtils.getHeader(file); 209 final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY); 210 if (null == version) { 211 // No version in the options : the format is unexpected 212 return false; 213 } 214 // Version 18 is the first one to include the whitelist 215 // Obviously this is a big ## HACK ## 216 return Integer.parseInt(version) >= 18; 217 } catch (java.io.FileNotFoundException e) { 218 return false; 219 } catch (java.io.IOException e) { 220 return false; 221 } catch (NumberFormatException e) { 222 return false; 223 } catch (BufferUnderflowException e) { 224 return false; 225 } catch (UnsupportedFormatException e) { 226 return false; 227 } 228 } 229 230 /** 231 * Returns a list of file addresses for a given locale, trying relevant methods in order. 232 * 233 * Tries to get binary dictionaries from various sources, in order: 234 * - Uses a content provider to get a public dictionary set, as per the protocol described 235 * in BinaryDictionaryFileDumper. 236 * If that fails: 237 * - Gets a file name from the built-in dictionary for this locale, if any. 238 * If that fails: 239 * - Returns null. 240 * @return The list of addresses of valid dictionary files, or null. 241 */ getDictionaryFiles(final Locale locale, final Context context, boolean notifyDictionaryPackForUpdates)242 public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale, 243 final Context context, boolean notifyDictionaryPackForUpdates) { 244 if (notifyDictionaryPackForUpdates) { 245 final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( 246 context, locale); 247 // It makes sure that the first time keyboard comes up and the dictionaries are reset, 248 // the DB is populated with the appropriate values for each locale. Helps in downloading 249 // the dictionaries when the user enables and switches new languages before the 250 // DictionaryService runs. 251 BinaryDictionaryFileDumper.downloadDictIfNeverRequested( 252 locale, context, hasDefaultWordList); 253 254 // Move a staging files to the cache ddirectories if any. 255 DictionaryInfoUtils.moveStagingFilesIfExists(context); 256 } 257 final File[] cachedWordLists = getCachedWordLists(locale.toString(), context); 258 final String mainDictId = DictionaryInfoUtils.getMainDictId(locale); 259 final DictPackSettings dictPackSettings = new DictPackSettings(context); 260 261 boolean foundMainDict = false; 262 final ArrayList<AssetFileAddress> fileList = new ArrayList<>(); 263 // cachedWordLists may not be null, see doc for getCachedDictionaryList 264 for (final File f : cachedWordLists) { 265 final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName()); 266 final boolean canUse = f.canRead() && hackCanUseDictionaryFile(f); 267 if (canUse && DictionaryInfoUtils.isMainWordListId(wordListId)) { 268 foundMainDict = true; 269 } 270 if (!dictPackSettings.isWordListActive(wordListId)) continue; 271 if (canUse) { 272 final AssetFileAddress afa = AssetFileAddress.makeFromFileName(f.getPath()); 273 if (null != afa) fileList.add(afa); 274 } else { 275 Log.e(TAG, "Found a cached dictionary file for " + locale.toString() 276 + " but cannot read or use it"); 277 } 278 } 279 280 if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) { 281 final int fallbackResId = 282 DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale); 283 final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId); 284 if (null != fallbackAsset) { 285 fileList.add(fallbackAsset); 286 } 287 } 288 289 return fileList; 290 } 291 } 292