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.ContentProviderClient; 20 import android.content.ContentResolver; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.res.AssetFileDescriptor; 24 import android.database.Cursor; 25 import android.net.Uri; 26 import android.os.RemoteException; 27 import android.text.TextUtils; 28 import android.util.Log; 29 30 import com.android.inputmethod.dictionarypack.DictionaryPackConstants; 31 import com.android.inputmethod.latin.DictionaryInfoUtils.DictionaryInfo; 32 33 import java.io.BufferedInputStream; 34 import java.io.BufferedOutputStream; 35 import java.io.File; 36 import java.io.FileNotFoundException; 37 import java.io.FileOutputStream; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.util.Arrays; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Locale; 45 46 /** 47 * Group class for static methods to help with creation and getting of the binary dictionary 48 * file from the dictionary provider 49 */ 50 public final class BinaryDictionaryFileDumper { 51 private static final String TAG = BinaryDictionaryFileDumper.class.getSimpleName(); 52 private static final boolean DEBUG = false; 53 54 /** 55 * The size of the temporary buffer to copy files. 56 */ 57 private static final int FILE_READ_BUFFER_SIZE = 8192; 58 // TODO: make the following data common with the native code 59 private static final byte[] MAGIC_NUMBER_VERSION_1 = 60 new byte[] { (byte)0x78, (byte)0xB1, (byte)0x00, (byte)0x00 }; 61 private static final byte[] MAGIC_NUMBER_VERSION_2 = 62 new byte[] { (byte)0x9B, (byte)0xC1, (byte)0x3A, (byte)0xFE }; 63 64 private static final String DICTIONARY_PROJECTION[] = { "id" }; 65 66 private static final String QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt"; 67 private static final String QUERY_PARAMETER_TRUE = "true"; 68 private static final String QUERY_PARAMETER_DELETE_RESULT = "result"; 69 private static final String QUERY_PARAMETER_SUCCESS = "success"; 70 private static final String QUERY_PARAMETER_FAILURE = "failure"; 71 72 // Using protocol version 2 to communicate with the dictionary pack 73 private static final String QUERY_PARAMETER_PROTOCOL = "protocol"; 74 private static final String QUERY_PARAMETER_PROTOCOL_VALUE = "2"; 75 76 // The path fragment to append after the client ID for dictionary info requests. 77 private static final String QUERY_PATH_DICT_INFO = "dict"; 78 // The path fragment to append after the client ID for dictionary datafile requests. 79 private static final String QUERY_PATH_DATAFILE = "datafile"; 80 // The path fragment to append after the client ID for updating the metadata URI. 81 private static final String QUERY_PATH_METADATA = "metadata"; 82 private static final String INSERT_METADATA_CLIENT_ID_COLUMN = "clientid"; 83 private static final String INSERT_METADATA_METADATA_URI_COLUMN = "uri"; 84 private static final String INSERT_METADATA_METADATA_ADDITIONAL_ID_COLUMN = "additionalid"; 85 86 // Prevents this class to be accidentally instantiated. BinaryDictionaryFileDumper()87 private BinaryDictionaryFileDumper() { 88 } 89 90 /** 91 * Returns a URI builder pointing to the dictionary pack. 92 * 93 * This creates a URI builder able to build a URI pointing to the dictionary 94 * pack content provider for a specific dictionary id. 95 */ getProviderUriBuilder(final String path)96 private static Uri.Builder getProviderUriBuilder(final String path) { 97 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 98 .authority(DictionaryPackConstants.AUTHORITY).appendPath(path); 99 } 100 101 /** 102 * Gets the content URI builder for a specified type. 103 * 104 * Supported types include QUERY_PATH_DICT_INFO, which takes the locale as 105 * the extraPath argument, and QUERY_PATH_DATAFILE, which needs a wordlist ID 106 * as the extraPath argument. 107 * 108 * @param clientId the clientId to use 109 * @param contentProviderClient the instance of content provider client 110 * @param queryPathType the path element encoding the type 111 * @param extraPath optional extra argument for this type (typically word list id) 112 * @return a builder that can build the URI for the best supported protocol version 113 * @throws RemoteException if the client can't be contacted 114 */ getContentUriBuilderForType(final String clientId, final ContentProviderClient contentProviderClient, final String queryPathType, final String extraPath)115 private static Uri.Builder getContentUriBuilderForType(final String clientId, 116 final ContentProviderClient contentProviderClient, final String queryPathType, 117 final String extraPath) throws RemoteException { 118 // Check whether protocol v2 is supported by building a v2 URI and calling getType() 119 // on it. If this returns null, v2 is not supported. 120 final Uri.Builder uriV2Builder = getProviderUriBuilder(clientId); 121 uriV2Builder.appendPath(queryPathType); 122 uriV2Builder.appendPath(extraPath); 123 uriV2Builder.appendQueryParameter(QUERY_PARAMETER_PROTOCOL, 124 QUERY_PARAMETER_PROTOCOL_VALUE); 125 if (null != contentProviderClient.getType(uriV2Builder.build())) return uriV2Builder; 126 // Protocol v2 is not supported, so create and return the protocol v1 uri. 127 return getProviderUriBuilder(extraPath); 128 } 129 130 /** 131 * Queries a content provider for the list of word lists for a specific locale 132 * available to copy into Latin IME. 133 */ getWordListWordListInfos(final Locale locale, final Context context, final boolean hasDefaultWordList)134 private static List<WordListInfo> getWordListWordListInfos(final Locale locale, 135 final Context context, final boolean hasDefaultWordList) { 136 final String clientId = context.getString(R.string.dictionary_pack_client_id); 137 final ContentProviderClient client = context.getContentResolver(). 138 acquireContentProviderClient(getProviderUriBuilder("").build()); 139 if (null == client) return Collections.<WordListInfo>emptyList(); 140 141 try { 142 final Uri.Builder builder = getContentUriBuilderForType(clientId, client, 143 QUERY_PATH_DICT_INFO, locale.toString()); 144 if (!hasDefaultWordList) { 145 builder.appendQueryParameter(QUERY_PARAMETER_MAY_PROMPT_USER, 146 QUERY_PARAMETER_TRUE); 147 } 148 final Uri queryUri = builder.build(); 149 final boolean isProtocolV2 = (QUERY_PARAMETER_PROTOCOL_VALUE.equals( 150 queryUri.getQueryParameter(QUERY_PARAMETER_PROTOCOL))); 151 152 Cursor c = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); 153 if (isProtocolV2 && null == c) { 154 reinitializeClientRecordInDictionaryContentProvider(context, client, clientId); 155 c = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); 156 } 157 if (null == c) return Collections.<WordListInfo>emptyList(); 158 if (c.getCount() <= 0 || !c.moveToFirst()) { 159 c.close(); 160 return Collections.<WordListInfo>emptyList(); 161 } 162 final ArrayList<WordListInfo> list = CollectionUtils.newArrayList(); 163 do { 164 final String wordListId = c.getString(0); 165 final String wordListLocale = c.getString(1); 166 if (TextUtils.isEmpty(wordListId)) continue; 167 list.add(new WordListInfo(wordListId, wordListLocale)); 168 } while (c.moveToNext()); 169 c.close(); 170 return list; 171 } catch (RemoteException e) { 172 // The documentation is unclear as to in which cases this may happen, but it probably 173 // happens when the content provider got suddenly killed because it crashed or because 174 // the user disabled it through Settings. 175 Log.e(TAG, "RemoteException: communication with the dictionary pack cut", e); 176 return Collections.<WordListInfo>emptyList(); 177 } catch (Exception e) { 178 // A crash here is dangerous because crashing here would brick any encrypted device - 179 // we need the keyboard to be up and working to enter the password, so we don't want 180 // to die no matter what. So let's be as safe as possible. 181 Log.e(TAG, "Unexpected exception communicating with the dictionary pack", e); 182 return Collections.<WordListInfo>emptyList(); 183 } finally { 184 client.release(); 185 } 186 } 187 188 189 /** 190 * Helper method to encapsulate exception handling. 191 */ openAssetFileDescriptor( final ContentProviderClient providerClient, final Uri uri)192 private static AssetFileDescriptor openAssetFileDescriptor( 193 final ContentProviderClient providerClient, final Uri uri) { 194 try { 195 return providerClient.openAssetFile(uri, "r"); 196 } catch (FileNotFoundException e) { 197 // I don't want to log the word list URI here for security concerns. The exception 198 // contains the name of the file, so let's not pass it to Log.e here. 199 Log.e(TAG, "Could not find a word list from the dictionary provider." 200 /* intentionally don't pass the exception (see comment above) */); 201 return null; 202 } catch (RemoteException e) { 203 Log.e(TAG, "Can't communicate with the dictionary pack", e); 204 return null; 205 } 206 } 207 208 /** 209 * Caches a word list the id of which is passed as an argument. This will write the file 210 * to the cache file name designated by its id and locale, overwriting it if already present 211 * and creating it (and its containing directory) if necessary. 212 */ cacheWordList(final String wordlistId, final String locale, final ContentProviderClient providerClient, final Context context)213 private static void cacheWordList(final String wordlistId, final String locale, 214 final ContentProviderClient providerClient, final Context context) { 215 final int COMPRESSED_CRYPTED_COMPRESSED = 0; 216 final int CRYPTED_COMPRESSED = 1; 217 final int COMPRESSED_CRYPTED = 2; 218 final int COMPRESSED_ONLY = 3; 219 final int CRYPTED_ONLY = 4; 220 final int NONE = 5; 221 final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED; 222 final int MODE_MAX = NONE; 223 224 final String clientId = context.getString(R.string.dictionary_pack_client_id); 225 final Uri.Builder wordListUriBuilder; 226 try { 227 wordListUriBuilder = getContentUriBuilderForType(clientId, 228 providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */); 229 } catch (RemoteException e) { 230 Log.e(TAG, "Can't communicate with the dictionary pack", e); 231 return; 232 } 233 final String finalFileName = 234 DictionaryInfoUtils.getCacheFileName(wordlistId, locale, context); 235 String tempFileName; 236 try { 237 tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context); 238 } catch (IOException e) { 239 Log.e(TAG, "Can't open the temporary file", e); 240 return; 241 } 242 243 for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) { 244 final InputStream originalSourceStream; 245 InputStream inputStream = null; 246 InputStream uncompressedStream = null; 247 InputStream decryptedStream = null; 248 BufferedInputStream bufferedInputStream = null; 249 File outputFile = null; 250 BufferedOutputStream bufferedOutputStream = null; 251 AssetFileDescriptor afd = null; 252 final Uri wordListUri = wordListUriBuilder.build(); 253 try { 254 // Open input. 255 afd = openAssetFileDescriptor(providerClient, wordListUri); 256 // If we can't open it at all, don't even try a number of times. 257 if (null == afd) return; 258 originalSourceStream = afd.createInputStream(); 259 // Open output. 260 outputFile = new File(tempFileName); 261 // Just to be sure, delete the file. This may fail silently, and return false: this 262 // is the right thing to do, as we just want to continue anyway. 263 outputFile.delete(); 264 // Get the appropriate decryption method for this try 265 switch (mode) { 266 case COMPRESSED_CRYPTED_COMPRESSED: 267 uncompressedStream = 268 FileTransforms.getUncompressedStream(originalSourceStream); 269 decryptedStream = FileTransforms.getDecryptedStream(uncompressedStream); 270 inputStream = FileTransforms.getUncompressedStream(decryptedStream); 271 break; 272 case CRYPTED_COMPRESSED: 273 decryptedStream = FileTransforms.getDecryptedStream(originalSourceStream); 274 inputStream = FileTransforms.getUncompressedStream(decryptedStream); 275 break; 276 case COMPRESSED_CRYPTED: 277 uncompressedStream = 278 FileTransforms.getUncompressedStream(originalSourceStream); 279 inputStream = FileTransforms.getDecryptedStream(uncompressedStream); 280 break; 281 case COMPRESSED_ONLY: 282 inputStream = FileTransforms.getUncompressedStream(originalSourceStream); 283 break; 284 case CRYPTED_ONLY: 285 inputStream = FileTransforms.getDecryptedStream(originalSourceStream); 286 break; 287 case NONE: 288 inputStream = originalSourceStream; 289 break; 290 } 291 bufferedInputStream = new BufferedInputStream(inputStream); 292 bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile)); 293 checkMagicAndCopyFileTo(bufferedInputStream, bufferedOutputStream); 294 bufferedOutputStream.flush(); 295 bufferedOutputStream.close(); 296 final File finalFile = new File(finalFileName); 297 finalFile.delete(); 298 if (!outputFile.renameTo(finalFile)) { 299 throw new IOException("Can't move the file to its final name"); 300 } 301 wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, 302 QUERY_PARAMETER_SUCCESS); 303 if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { 304 Log.e(TAG, "Could not have the dictionary pack delete a word list"); 305 } 306 BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile); 307 // Success! Close files (through the finally{} clause) and return. 308 return; 309 } catch (Exception e) { 310 if (DEBUG) { 311 Log.i(TAG, "Can't open word list in mode " + mode, e); 312 } 313 if (null != outputFile) { 314 // This may or may not fail. The file may not have been created if the 315 // exception was thrown before it could be. Hence, both failure and 316 // success are expected outcomes, so we don't check the return value. 317 outputFile.delete(); 318 } 319 // Try the next method. 320 } finally { 321 // Ignore exceptions while closing files. 322 try { 323 if (null != afd) afd.close(); 324 if (null != inputStream) inputStream.close(); 325 if (null != uncompressedStream) uncompressedStream.close(); 326 if (null != decryptedStream) decryptedStream.close(); 327 if (null != bufferedInputStream) bufferedInputStream.close(); 328 } catch (Exception e) { 329 Log.e(TAG, "Exception while closing a file descriptor", e); 330 } 331 try { 332 if (null != bufferedOutputStream) bufferedOutputStream.close(); 333 } catch (Exception e) { 334 Log.e(TAG, "Exception while closing a file", e); 335 } 336 } 337 } 338 339 // We could not copy the file at all. This is very unexpected. 340 // I'd rather not print the word list ID to the log out of security concerns 341 Log.e(TAG, "Could not copy a word list. Will not be able to use it."); 342 // If we can't copy it we should warn the dictionary provider so that it can mark it 343 // as invalid. 344 wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, 345 QUERY_PARAMETER_FAILURE); 346 try { 347 if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { 348 Log.e(TAG, "In addition, we were unable to delete it."); 349 } 350 } catch (RemoteException e) { 351 Log.e(TAG, "In addition, communication with the dictionary provider was cut", e); 352 } 353 } 354 355 /** 356 * Queries a content provider for word list data for some locale and cache the returned files 357 * 358 * This will query a content provider for word list data for a given locale, and copy the 359 * files locally so that they can be mmap'ed. This may overwrite previously cached word lists 360 * with newer versions if a newer version is made available by the content provider. 361 * @throw FileNotFoundException if the provider returns non-existent data. 362 * @throw IOException if the provider-returned data could not be read. 363 */ cacheWordListsFromContentProvider(final Locale locale, final Context context, final boolean hasDefaultWordList)364 public static void cacheWordListsFromContentProvider(final Locale locale, 365 final Context context, final boolean hasDefaultWordList) { 366 final ContentProviderClient providerClient = context.getContentResolver(). 367 acquireContentProviderClient(getProviderUriBuilder("").build()); 368 if (null == providerClient) { 369 Log.e(TAG, "Can't establish communication with the dictionary provider"); 370 return; 371 } 372 try { 373 final List<WordListInfo> idList = getWordListWordListInfos(locale, context, 374 hasDefaultWordList); 375 for (WordListInfo id : idList) { 376 cacheWordList(id.mId, id.mLocale, providerClient, context); 377 } 378 } finally { 379 providerClient.release(); 380 } 381 } 382 383 /** 384 * Copies the data in an input stream to a target file if the magic number matches. 385 * 386 * If the magic number does not match the expected value, this method throws an 387 * IOException. Other usual conditions for IOException or FileNotFoundException 388 * also apply. 389 * 390 * @param input the stream to be copied. 391 * @param output an output stream to copy the data to. 392 */ checkMagicAndCopyFileTo(final BufferedInputStream input, final BufferedOutputStream output)393 public static void checkMagicAndCopyFileTo(final BufferedInputStream input, 394 final BufferedOutputStream output) throws FileNotFoundException, IOException { 395 // Check the magic number 396 final int length = MAGIC_NUMBER_VERSION_2.length; 397 final byte[] magicNumberBuffer = new byte[length]; 398 final int readMagicNumberSize = input.read(magicNumberBuffer, 0, length); 399 if (readMagicNumberSize < length) { 400 throw new IOException("Less bytes to read than the magic number length"); 401 } 402 if (!Arrays.equals(MAGIC_NUMBER_VERSION_2, magicNumberBuffer)) { 403 if (!Arrays.equals(MAGIC_NUMBER_VERSION_1, magicNumberBuffer)) { 404 throw new IOException("Wrong magic number for downloaded file"); 405 } 406 } 407 output.write(magicNumberBuffer); 408 409 // Actually copy the file 410 final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE]; 411 for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) 412 output.write(buffer, 0, readBytes); 413 input.close(); 414 } 415 reinitializeClientRecordInDictionaryContentProvider(final Context context, final ContentProviderClient client, final String clientId)416 private static void reinitializeClientRecordInDictionaryContentProvider(final Context context, 417 final ContentProviderClient client, final String clientId) throws RemoteException { 418 final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context); 419 final String metadataAdditionalId = MetadataFileUriGetter.getMetadataAdditionalId(context); 420 if (TextUtils.isEmpty(metadataFileUri)) return; 421 // Tell the content provider to reset all information about this client id 422 final Uri metadataContentUri = getProviderUriBuilder(clientId) 423 .appendPath(QUERY_PATH_METADATA) 424 .appendQueryParameter(QUERY_PARAMETER_PROTOCOL, QUERY_PARAMETER_PROTOCOL_VALUE) 425 .build(); 426 client.delete(metadataContentUri, null, null); 427 // Update the metadata URI 428 final ContentValues metadataValues = new ContentValues(); 429 metadataValues.put(INSERT_METADATA_CLIENT_ID_COLUMN, clientId); 430 metadataValues.put(INSERT_METADATA_METADATA_URI_COLUMN, metadataFileUri); 431 metadataValues.put(INSERT_METADATA_METADATA_ADDITIONAL_ID_COLUMN, metadataAdditionalId); 432 client.insert(metadataContentUri, metadataValues); 433 434 // Update the dictionary list. 435 final Uri dictionaryContentUriBase = getProviderUriBuilder(clientId) 436 .appendPath(QUERY_PATH_DICT_INFO) 437 .appendQueryParameter(QUERY_PARAMETER_PROTOCOL, QUERY_PARAMETER_PROTOCOL_VALUE) 438 .build(); 439 final ArrayList<DictionaryInfo> dictionaryList = 440 DictionaryInfoUtils.getCurrentDictionaryFileNameAndVersionInfo(context); 441 final int length = dictionaryList.size(); 442 for (int i = 0; i < length; ++i) { 443 final DictionaryInfo info = dictionaryList.get(i); 444 client.insert(Uri.withAppendedPath(dictionaryContentUriBase, info.mId), 445 info.toContentValues()); 446 } 447 } 448 449 /** 450 * Initialize a client record with the dictionary content provider. 451 * 452 * This merely acquires the content provider and calls 453 * #reinitializeClientRecordInDictionaryContentProvider. 454 * 455 * @param context the context for resources and providers. 456 * @param clientId the client ID to use. 457 */ initializeClientRecordHelper(final Context context, final String clientId)458 public static void initializeClientRecordHelper(final Context context, 459 final String clientId) { 460 try { 461 final ContentProviderClient client = context.getContentResolver(). 462 acquireContentProviderClient(getProviderUriBuilder("").build()); 463 if (null == client) return; 464 reinitializeClientRecordInDictionaryContentProvider(context, client, clientId); 465 } catch (RemoteException e) { 466 Log.e(TAG, "Cannot contact the dictionary content provider", e); 467 } 468 } 469 } 470