• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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