• 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"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.inputmethod.latin;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.content.res.AssetFileDescriptor;
22 import android.database.Cursor;
23 import android.net.Uri;
24 import android.text.TextUtils;
25 import android.util.Log;
26 
27 import java.io.BufferedInputStream;
28 import java.io.File;
29 import java.io.FileNotFoundException;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Locale;
38 
39 /**
40  * Group class for static methods to help with creation and getting of the binary dictionary
41  * file from the dictionary provider
42  */
43 public class BinaryDictionaryFileDumper {
44     private static final String TAG = BinaryDictionaryFileDumper.class.getSimpleName();
45     private static final boolean DEBUG = false;
46 
47     /**
48      * The size of the temporary buffer to copy files.
49      */
50     private static final int FILE_READ_BUFFER_SIZE = 1024;
51     // TODO: make the following data common with the native code
52     private static final byte[] MAGIC_NUMBER = new byte[] { 0x78, (byte)0xB1 };
53 
54     private static final String DICTIONARY_PROJECTION[] = { "id" };
55 
56     // Prevents this class to be accidentally instantiated.
BinaryDictionaryFileDumper()57     private BinaryDictionaryFileDumper() {
58     }
59 
60     /**
61      * Return for a given locale or dictionary id the provider URI to get the dictionary.
62      */
getProviderUri(String path)63     private static Uri getProviderUri(String path) {
64         return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
65                 .authority(BinaryDictionary.DICTIONARY_PACK_AUTHORITY).appendPath(
66                         path).build();
67     }
68 
69     /**
70      * Queries a content provider for the list of word lists for a specific locale
71      * available to copy into Latin IME.
72      */
getWordListWordListInfos(final Locale locale, final Context context)73     private static List<WordListInfo> getWordListWordListInfos(final Locale locale,
74             final Context context) {
75         final ContentResolver resolver = context.getContentResolver();
76         final Uri dictionaryPackUri = getProviderUri(locale.toString());
77 
78         final Cursor c = resolver.query(dictionaryPackUri, DICTIONARY_PROJECTION, null, null, null);
79         if (null == c) return Collections.<WordListInfo>emptyList();
80         if (c.getCount() <= 0 || !c.moveToFirst()) {
81             c.close();
82             return Collections.<WordListInfo>emptyList();
83         }
84 
85         try {
86             final List<WordListInfo> list = new ArrayList<WordListInfo>();
87             do {
88                 final String wordListId = c.getString(0);
89                 final String wordListLocale = c.getString(1);
90                 if (TextUtils.isEmpty(wordListId)) continue;
91                 list.add(new WordListInfo(wordListId, wordListLocale));
92             } while (c.moveToNext());
93             c.close();
94             return list;
95         } catch (Exception e) {
96             // Just in case we hit a problem in communication with the dictionary pack.
97             // We don't want to die.
98             Log.e(TAG, "Exception communicating with the dictionary pack : " + e);
99             return Collections.<WordListInfo>emptyList();
100         }
101     }
102 
103 
104     /**
105      * Helper method to encapsulate exception handling.
106      */
openAssetFileDescriptor(final ContentResolver resolver, final Uri uri)107     private static AssetFileDescriptor openAssetFileDescriptor(final ContentResolver resolver,
108             final Uri uri) {
109         try {
110             return resolver.openAssetFileDescriptor(uri, "r");
111         } catch (FileNotFoundException e) {
112             // I don't want to log the word list URI here for security concerns
113             Log.e(TAG, "Could not find a word list from the dictionary provider.");
114             return null;
115         }
116     }
117 
118     /**
119      * Caches a word list the id of which is passed as an argument. This will write the file
120      * to the cache file name designated by its id and locale, overwriting it if already present
121      * and creating it (and its containing directory) if necessary.
122      */
cacheWordList(final String id, final String locale, final ContentResolver resolver, final Context context)123     private static AssetFileAddress cacheWordList(final String id, final String locale,
124             final ContentResolver resolver, final Context context) {
125 
126         final int COMPRESSED_CRYPTED_COMPRESSED = 0;
127         final int CRYPTED_COMPRESSED = 1;
128         final int COMPRESSED_CRYPTED = 2;
129         final int COMPRESSED_ONLY = 3;
130         final int CRYPTED_ONLY = 4;
131         final int NONE = 5;
132         final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED;
133         final int MODE_MAX = NONE;
134 
135         final Uri wordListUri = getProviderUri(id);
136         final String outputFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context);
137 
138         for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) {
139             InputStream originalSourceStream = null;
140             InputStream inputStream = null;
141             File outputFile = null;
142             FileOutputStream outputStream = null;
143             AssetFileDescriptor afd = null;
144             try {
145                 // Open input.
146                 afd = openAssetFileDescriptor(resolver, wordListUri);
147                 // If we can't open it at all, don't even try a number of times.
148                 if (null == afd) return null;
149                 originalSourceStream = afd.createInputStream();
150                 // Open output.
151                 outputFile = new File(outputFileName);
152                 outputStream = new FileOutputStream(outputFile);
153                 // Get the appropriate decryption method for this try
154                 switch (mode) {
155                     case COMPRESSED_CRYPTED_COMPRESSED:
156                         inputStream = FileTransforms.getUncompressedStream(
157                                 FileTransforms.getDecryptedStream(
158                                         FileTransforms.getUncompressedStream(
159                                                 originalSourceStream)));
160                         break;
161                     case CRYPTED_COMPRESSED:
162                         inputStream = FileTransforms.getUncompressedStream(
163                                 FileTransforms.getDecryptedStream(originalSourceStream));
164                         break;
165                     case COMPRESSED_CRYPTED:
166                         inputStream = FileTransforms.getDecryptedStream(
167                                 FileTransforms.getUncompressedStream(originalSourceStream));
168                         break;
169                     case COMPRESSED_ONLY:
170                         inputStream = FileTransforms.getUncompressedStream(originalSourceStream);
171                         break;
172                     case CRYPTED_ONLY:
173                         inputStream = FileTransforms.getDecryptedStream(originalSourceStream);
174                         break;
175                     case NONE:
176                         inputStream = originalSourceStream;
177                         break;
178                     }
179                 checkMagicAndCopyFileTo(new BufferedInputStream(inputStream), outputStream);
180                 if (0 >= resolver.delete(wordListUri, null, null)) {
181                     Log.e(TAG, "Could not have the dictionary pack delete a word list");
182                 }
183                 // Success! Close files (through the finally{} clause) and return.
184                 return AssetFileAddress.makeFromFileName(outputFileName);
185             } catch (Exception e) {
186                 if (DEBUG) {
187                     Log.i(TAG, "Can't open word list in mode " + mode + " : " + e);
188                 }
189                 if (null != outputFile) {
190                     // This may or may not fail. The file may not have been created if the
191                     // exception was thrown before it could be. Hence, both failure and
192                     // success are expected outcomes, so we don't check the return value.
193                     outputFile.delete();
194                 }
195                 // Try the next method.
196             } finally {
197                 // Ignore exceptions while closing files.
198                 try {
199                     // afd.close() will close inputStream, we should not call inputStream.close().
200                     if (null != afd) afd.close();
201                 } catch (Exception e) {
202                     Log.e(TAG, "Exception while closing a cross-process file descriptor : " + e);
203                 }
204                 try {
205                     if (null != outputStream) outputStream.close();
206                 } catch (Exception e) {
207                     Log.e(TAG, "Exception while closing a file : " + e);
208                 }
209             }
210         }
211 
212         // We could not copy the file at all. This is very unexpected.
213         // I'd rather not print the word list ID to the log out of security concerns
214         Log.e(TAG, "Could not copy a word list. Will not be able to use it.");
215         // If we can't copy it we should probably delete it to avoid trying to copy it over
216         // and over each time we open LatinIME.
217         if (0 >= resolver.delete(wordListUri, null, null)) {
218             Log.e(TAG, "In addition, we were unable to delete it.");
219         }
220         return null;
221     }
222 
223     /**
224      * Queries a content provider for word list data for some locale and cache the returned files
225      *
226      * This will query a content provider for word list data for a given locale, and copy the
227      * files locally so that they can be mmap'ed. This may overwrite previously cached word lists
228      * with newer versions if a newer version is made available by the content provider.
229      * @returns the addresses of the word list files, or null if no data could be obtained.
230      * @throw FileNotFoundException if the provider returns non-existent data.
231      * @throw IOException if the provider-returned data could not be read.
232      */
cacheWordListsFromContentProvider(final Locale locale, final Context context)233     public static List<AssetFileAddress> cacheWordListsFromContentProvider(final Locale locale,
234             final Context context) {
235         final ContentResolver resolver = context.getContentResolver();
236         final List<WordListInfo> idList = getWordListWordListInfos(locale, context);
237         final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>();
238         for (WordListInfo id : idList) {
239             final AssetFileAddress afd = cacheWordList(id.mId, id.mLocale, resolver, context);
240             if (null != afd) {
241                 fileAddressList.add(afd);
242             }
243         }
244         return fileAddressList;
245     }
246 
247     /**
248      * Copies the data in an input stream to a target file if the magic number matches.
249      *
250      * If the magic number does not match the expected value, this method throws an
251      * IOException. Other usual conditions for IOException or FileNotFoundException
252      * also apply.
253      *
254      * @param input the stream to be copied.
255      * @param outputFile an outputstream to copy the data to.
256      */
checkMagicAndCopyFileTo(final BufferedInputStream input, final FileOutputStream output)257     private static void checkMagicAndCopyFileTo(final BufferedInputStream input,
258             final FileOutputStream output) throws FileNotFoundException, IOException {
259         // Check the magic number
260         final byte[] magicNumberBuffer = new byte[MAGIC_NUMBER.length];
261         final int readMagicNumberSize = input.read(magicNumberBuffer, 0, MAGIC_NUMBER.length);
262         if (readMagicNumberSize < MAGIC_NUMBER.length) {
263             throw new IOException("Less bytes to read than the magic number length");
264         }
265         if (!Arrays.equals(MAGIC_NUMBER, magicNumberBuffer)) {
266             throw new IOException("Wrong magic number for downloaded file");
267         }
268         output.write(MAGIC_NUMBER);
269 
270         // Actually copy the file
271         final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE];
272         for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer))
273             output.write(buffer, 0, readBytes);
274         input.close();
275     }
276 }
277