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