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