BinaryDictionaryGetter.java revision 80e0bf04292867ddc769aca75ebaee817b95a941
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.content.Context; 20import android.content.SharedPreferences; 21import android.content.pm.PackageManager.NameNotFoundException; 22import android.content.res.AssetFileDescriptor; 23import android.content.res.Resources; 24import android.util.Log; 25 26import java.io.File; 27import java.io.FileNotFoundException; 28import java.io.IOException; 29import java.util.Arrays; 30import java.util.ArrayList; 31import java.util.List; 32import java.util.Locale; 33 34/** 35 * Helper class to get the address of a mmap'able dictionary file. 36 */ 37class BinaryDictionaryGetter { 38 39 /** 40 * Used for Log actions from this class 41 */ 42 private static final String TAG = BinaryDictionaryGetter.class.getSimpleName(); 43 44 /** 45 * Name of the common preferences name to know which word list are on and which are off. 46 */ 47 private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"; 48 49 // Prevents this from being instantiated 50 private BinaryDictionaryGetter() {} 51 52 /** 53 * Returns whether we may want to use this character as part of a file name. 54 * 55 * This basically only accepts ascii letters and numbers, and rejects everything else. 56 */ 57 private static boolean isFileNameCharacter(int codePoint) { 58 if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit 59 if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase 60 if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase 61 return codePoint == '_'; // Underscore 62 } 63 64 /** 65 * Escapes a string for any characters that may be suspicious for a file or directory name. 66 * 67 * Concretely this does a sort of URL-encoding except it will encode everything that's not 68 * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which 69 * we cannot allow here) 70 */ 71 // TODO: create a unit test for this method 72 private static String replaceFileNameDangerousCharacters(final String name) { 73 // This assumes '%' is fully available as a non-separator, normal 74 // character in a file name. This is probably true for all file systems. 75 final StringBuilder sb = new StringBuilder(); 76 for (int i = 0; i < name.length(); ++i) { 77 final int codePoint = name.codePointAt(i); 78 if (isFileNameCharacter(codePoint)) { 79 sb.appendCodePoint(codePoint); 80 } else { 81 // 6 digits - unicode is limited to 21 bits 82 sb.append(String.format((Locale)null, "%%%1$06x", codePoint)); 83 } 84 } 85 return sb.toString(); 86 } 87 88 /** 89 * Reverse escaping done by replaceFileNameDangerousCharacters. 90 */ 91 private static String getWordListIdFromFileName(final String fname) { 92 final StringBuilder sb = new StringBuilder(); 93 for (int i = 0; i < fname.length(); ++i) { 94 final int codePoint = fname.codePointAt(i); 95 if ('%' != codePoint) { 96 sb.appendCodePoint(codePoint); 97 } else { 98 final int encodedCodePoint = Integer.parseInt(fname.substring(i + 1, i + 7), 16); 99 i += 6; 100 sb.appendCodePoint(encodedCodePoint); 101 } 102 } 103 return sb.toString(); 104 } 105 106 /** 107 * Find out the cache directory associated with a specific locale. 108 */ 109 private static String getCacheDirectoryForLocale(Locale locale, Context context) { 110 final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale.toString()); 111 final String absoluteDirectoryName = context.getFilesDir() + File.separator 112 + relativeDirectoryName; 113 final File directory = new File(absoluteDirectoryName); 114 if (!directory.exists()) { 115 if (!directory.mkdirs()) { 116 Log.e(TAG, "Could not create the directory for locale" + locale); 117 } 118 } 119 return absoluteDirectoryName; 120 } 121 122 /** 123 * Generates a file name for the id and locale passed as an argument. 124 * 125 * In the current implementation the file name returned will always be unique for 126 * any id/locale pair, but please do not expect that the id can be the same for 127 * different dictionaries with different locales. An id should be unique for any 128 * dictionary. 129 * The file name is pretty much an URL-encoded version of the id inside a directory 130 * named like the locale, except it will also escape characters that look dangerous 131 * to some file systems. 132 * @param id the id of the dictionary for which to get a file name 133 * @param locale the locale for which to get the file name 134 * @param context the context to use for getting the directory 135 * @return the name of the file to be created 136 */ 137 public static String getCacheFileName(String id, Locale locale, Context context) { 138 final String fileName = replaceFileNameDangerousCharacters(id); 139 return getCacheDirectoryForLocale(locale, context) + File.separator + fileName; 140 } 141 142 /** 143 * Returns a file address from a resource, or null if it cannot be opened. 144 */ 145 private static AssetFileAddress loadFallbackResource(final Context context, 146 final int fallbackResId, final Locale locale) { 147 final Resources res = context.getResources(); 148 final Locale savedLocale = Utils.setSystemLocale(res, locale); 149 final AssetFileDescriptor afd = res.openRawResourceFd(fallbackResId); 150 Utils.setSystemLocale(res, savedLocale); 151 152 if (afd == null) { 153 Log.e(TAG, "Found the resource but cannot read it. Is it compressed? resId=" 154 + fallbackResId); 155 return null; 156 } 157 return AssetFileAddress.makeFromFileNameAndOffset( 158 context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength()); 159 } 160 161 /** 162 * Returns the list of cached files for a specific locale. 163 * 164 * @param locale the locale to find the dictionary files for. 165 * @param context the context on which to open the files upon. 166 * @return a list of binary dictionary files, which may be null but may not be empty. 167 */ 168 private static List<AssetFileAddress> getCachedDictionaryList(final Locale locale, 169 final Context context) { 170 final String directoryName = getCacheDirectoryForLocale(locale, context); 171 final File[] cacheFiles = new File(directoryName).listFiles(); 172 // TODO: Never return null. Fallback on the built-in dictionary, and if that's 173 // not present or disabled, then return an empty list. 174 if (null == cacheFiles) return null; 175 176 final SharedPreferences dictPackSettings; 177 try { 178 final String dictPackName = context.getString(R.string.dictionary_pack_package_name); 179 final Context dictPackContext = context.createPackageContext(dictPackName, 0); 180 dictPackSettings = dictPackContext.getSharedPreferences(COMMON_PREFERENCES_NAME, 181 Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS); 182 } catch (NameNotFoundException e) { 183 // The dictionary pack is not installed... 184 // TODO: fallback on the built-in dict, see the TODO above 185 Log.e(TAG, "Could not find a dictionary pack"); 186 return null; 187 } 188 189 final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>(); 190 for (File f : cacheFiles) { 191 final String wordListId = getWordListIdFromFileName(f.getName()); 192 final boolean isActive = dictPackSettings.getBoolean(wordListId, true); 193 if (!isActive) continue; 194 if (f.canRead()) { 195 fileList.add(AssetFileAddress.makeFromFileName(f.getPath())); 196 } else { 197 Log.e(TAG, "Found a cached dictionary file but cannot read it"); 198 } 199 } 200 return fileList.size() > 0 ? fileList : null; 201 } 202 203 /** 204 * Returns a list of file addresses for a given locale, trying relevant methods in order. 205 * 206 * Tries to get binary dictionaries from various sources, in order: 207 * - Uses a content provider to get a public dictionary set, as per the protocol described 208 * in BinaryDictionaryFileDumper. 209 * If that fails: 210 * - Gets a file name from the fallback resource passed as an argument. 211 * If that fails: 212 * - Returns null. 213 * @return The address of a valid file, or null. 214 */ 215 public static List<AssetFileAddress> getDictionaryFiles(final Locale locale, 216 final Context context, final int fallbackResId) { 217 218 // cacheDictionariesFromContentProvider returns the list of files it copied to local 219 // storage, but we don't really care about what was copied NOW: what we want is the 220 // list of everything we ever cached, so we ignore the return value. 221 BinaryDictionaryFileDumper.cacheDictionariesFromContentProvider(locale, context); 222 List<AssetFileAddress> cachedDictionaryList = getCachedDictionaryList(locale, context); 223 if (null != cachedDictionaryList) { 224 return cachedDictionaryList; 225 } 226 227 final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId, 228 locale); 229 if (null == fallbackAsset) return null; 230 return Arrays.asList(fallbackAsset); 231 } 232} 233