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 17 package com.android.inputmethod.latin; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.res.AssetFileDescriptor; 22 import android.database.Cursor; 23 import android.net.Uri; 24 import android.text.TextUtils; 25 import android.util.Log; 26 27 import java.io.BufferedInputStream; 28 import java.io.File; 29 import java.io.FileNotFoundException; 30 import java.io.FileOutputStream; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.Locale; 38 39 /** 40 * Group class for static methods to help with creation and getting of the binary dictionary 41 * file from the dictionary provider 42 */ 43 public class BinaryDictionaryFileDumper { 44 private static final String TAG = BinaryDictionaryFileDumper.class.getSimpleName(); 45 private static final boolean DEBUG = false; 46 47 /** 48 * The size of the temporary buffer to copy files. 49 */ 50 private static final int FILE_READ_BUFFER_SIZE = 1024; 51 // TODO: make the following data common with the native code 52 private static final byte[] MAGIC_NUMBER = new byte[] { 0x78, (byte)0xB1 }; 53 54 private static final String DICTIONARY_PROJECTION[] = { "id" }; 55 56 // Prevents this class to be accidentally instantiated. BinaryDictionaryFileDumper()57 private BinaryDictionaryFileDumper() { 58 } 59 60 /** 61 * Return for a given locale or dictionary id the provider URI to get the dictionary. 62 */ getProviderUri(String path)63 private static Uri getProviderUri(String path) { 64 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 65 .authority(BinaryDictionary.DICTIONARY_PACK_AUTHORITY).appendPath( 66 path).build(); 67 } 68 69 /** 70 * Queries a content provider for the list of word lists for a specific locale 71 * available to copy into Latin IME. 72 */ getWordListWordListInfos(final Locale locale, final Context context)73 private static List<WordListInfo> getWordListWordListInfos(final Locale locale, 74 final Context context) { 75 final ContentResolver resolver = context.getContentResolver(); 76 final Uri dictionaryPackUri = getProviderUri(locale.toString()); 77 78 final Cursor c = resolver.query(dictionaryPackUri, DICTIONARY_PROJECTION, null, null, null); 79 if (null == c) return Collections.<WordListInfo>emptyList(); 80 if (c.getCount() <= 0 || !c.moveToFirst()) { 81 c.close(); 82 return Collections.<WordListInfo>emptyList(); 83 } 84 85 try { 86 final List<WordListInfo> list = new ArrayList<WordListInfo>(); 87 do { 88 final String wordListId = c.getString(0); 89 final String wordListLocale = c.getString(1); 90 if (TextUtils.isEmpty(wordListId)) continue; 91 list.add(new WordListInfo(wordListId, wordListLocale)); 92 } while (c.moveToNext()); 93 c.close(); 94 return list; 95 } catch (Exception e) { 96 // Just in case we hit a problem in communication with the dictionary pack. 97 // We don't want to die. 98 Log.e(TAG, "Exception communicating with the dictionary pack : " + e); 99 return Collections.<WordListInfo>emptyList(); 100 } 101 } 102 103 104 /** 105 * Helper method to encapsulate exception handling. 106 */ openAssetFileDescriptor(final ContentResolver resolver, final Uri uri)107 private static AssetFileDescriptor openAssetFileDescriptor(final ContentResolver resolver, 108 final Uri uri) { 109 try { 110 return resolver.openAssetFileDescriptor(uri, "r"); 111 } catch (FileNotFoundException e) { 112 // I don't want to log the word list URI here for security concerns 113 Log.e(TAG, "Could not find a word list from the dictionary provider."); 114 return null; 115 } 116 } 117 118 /** 119 * Caches a word list the id of which is passed as an argument. This will write the file 120 * to the cache file name designated by its id and locale, overwriting it if already present 121 * and creating it (and its containing directory) if necessary. 122 */ cacheWordList(final String id, final String locale, final ContentResolver resolver, final Context context)123 private static AssetFileAddress cacheWordList(final String id, final String locale, 124 final ContentResolver resolver, final Context context) { 125 126 final int COMPRESSED_CRYPTED_COMPRESSED = 0; 127 final int CRYPTED_COMPRESSED = 1; 128 final int COMPRESSED_CRYPTED = 2; 129 final int COMPRESSED_ONLY = 3; 130 final int CRYPTED_ONLY = 4; 131 final int NONE = 5; 132 final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED; 133 final int MODE_MAX = NONE; 134 135 final Uri wordListUri = getProviderUri(id); 136 final String outputFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context); 137 138 for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) { 139 InputStream originalSourceStream = null; 140 InputStream inputStream = null; 141 File outputFile = null; 142 FileOutputStream outputStream = null; 143 AssetFileDescriptor afd = null; 144 try { 145 // Open input. 146 afd = openAssetFileDescriptor(resolver, wordListUri); 147 // If we can't open it at all, don't even try a number of times. 148 if (null == afd) return null; 149 originalSourceStream = afd.createInputStream(); 150 // Open output. 151 outputFile = new File(outputFileName); 152 outputStream = new FileOutputStream(outputFile); 153 // Get the appropriate decryption method for this try 154 switch (mode) { 155 case COMPRESSED_CRYPTED_COMPRESSED: 156 inputStream = FileTransforms.getUncompressedStream( 157 FileTransforms.getDecryptedStream( 158 FileTransforms.getUncompressedStream( 159 originalSourceStream))); 160 break; 161 case CRYPTED_COMPRESSED: 162 inputStream = FileTransforms.getUncompressedStream( 163 FileTransforms.getDecryptedStream(originalSourceStream)); 164 break; 165 case COMPRESSED_CRYPTED: 166 inputStream = FileTransforms.getDecryptedStream( 167 FileTransforms.getUncompressedStream(originalSourceStream)); 168 break; 169 case COMPRESSED_ONLY: 170 inputStream = FileTransforms.getUncompressedStream(originalSourceStream); 171 break; 172 case CRYPTED_ONLY: 173 inputStream = FileTransforms.getDecryptedStream(originalSourceStream); 174 break; 175 case NONE: 176 inputStream = originalSourceStream; 177 break; 178 } 179 checkMagicAndCopyFileTo(new BufferedInputStream(inputStream), outputStream); 180 if (0 >= resolver.delete(wordListUri, null, null)) { 181 Log.e(TAG, "Could not have the dictionary pack delete a word list"); 182 } 183 // Success! Close files (through the finally{} clause) and return. 184 return AssetFileAddress.makeFromFileName(outputFileName); 185 } catch (Exception e) { 186 if (DEBUG) { 187 Log.i(TAG, "Can't open word list in mode " + mode + " : " + e); 188 } 189 if (null != outputFile) { 190 // This may or may not fail. The file may not have been created if the 191 // exception was thrown before it could be. Hence, both failure and 192 // success are expected outcomes, so we don't check the return value. 193 outputFile.delete(); 194 } 195 // Try the next method. 196 } finally { 197 // Ignore exceptions while closing files. 198 try { 199 // afd.close() will close inputStream, we should not call inputStream.close(). 200 if (null != afd) afd.close(); 201 } catch (Exception e) { 202 Log.e(TAG, "Exception while closing a cross-process file descriptor : " + e); 203 } 204 try { 205 if (null != outputStream) outputStream.close(); 206 } catch (Exception e) { 207 Log.e(TAG, "Exception while closing a file : " + e); 208 } 209 } 210 } 211 212 // We could not copy the file at all. This is very unexpected. 213 // I'd rather not print the word list ID to the log out of security concerns 214 Log.e(TAG, "Could not copy a word list. Will not be able to use it."); 215 // If we can't copy it we should probably delete it to avoid trying to copy it over 216 // and over each time we open LatinIME. 217 if (0 >= resolver.delete(wordListUri, null, null)) { 218 Log.e(TAG, "In addition, we were unable to delete it."); 219 } 220 return null; 221 } 222 223 /** 224 * Queries a content provider for word list data for some locale and cache the returned files 225 * 226 * This will query a content provider for word list data for a given locale, and copy the 227 * files locally so that they can be mmap'ed. This may overwrite previously cached word lists 228 * with newer versions if a newer version is made available by the content provider. 229 * @returns the addresses of the word list files, or null if no data could be obtained. 230 * @throw FileNotFoundException if the provider returns non-existent data. 231 * @throw IOException if the provider-returned data could not be read. 232 */ cacheWordListsFromContentProvider(final Locale locale, final Context context)233 public static List<AssetFileAddress> cacheWordListsFromContentProvider(final Locale locale, 234 final Context context) { 235 final ContentResolver resolver = context.getContentResolver(); 236 final List<WordListInfo> idList = getWordListWordListInfos(locale, context); 237 final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>(); 238 for (WordListInfo id : idList) { 239 final AssetFileAddress afd = cacheWordList(id.mId, id.mLocale, resolver, context); 240 if (null != afd) { 241 fileAddressList.add(afd); 242 } 243 } 244 return fileAddressList; 245 } 246 247 /** 248 * Copies the data in an input stream to a target file if the magic number matches. 249 * 250 * If the magic number does not match the expected value, this method throws an 251 * IOException. Other usual conditions for IOException or FileNotFoundException 252 * also apply. 253 * 254 * @param input the stream to be copied. 255 * @param outputFile an outputstream to copy the data to. 256 */ checkMagicAndCopyFileTo(final BufferedInputStream input, final FileOutputStream output)257 private static void checkMagicAndCopyFileTo(final BufferedInputStream input, 258 final FileOutputStream output) throws FileNotFoundException, IOException { 259 // Check the magic number 260 final byte[] magicNumberBuffer = new byte[MAGIC_NUMBER.length]; 261 final int readMagicNumberSize = input.read(magicNumberBuffer, 0, MAGIC_NUMBER.length); 262 if (readMagicNumberSize < MAGIC_NUMBER.length) { 263 throw new IOException("Less bytes to read than the magic number length"); 264 } 265 if (!Arrays.equals(MAGIC_NUMBER, magicNumberBuffer)) { 266 throw new IOException("Wrong magic number for downloaded file"); 267 } 268 output.write(MAGIC_NUMBER); 269 270 // Actually copy the file 271 final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE]; 272 for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) 273 output.write(buffer, 0, readBytes); 274 input.close(); 275 } 276 } 277