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