BinaryDictionaryFileDumper.java revision a16621ada43c7b499857bc8967e454994098bff3
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
17package com.android.inputmethod.latin;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.res.AssetFileDescriptor;
22import android.content.res.Resources;
23import android.database.Cursor;
24import android.net.Uri;
25import android.text.TextUtils;
26import android.util.Log;
27
28import java.io.FileInputStream;
29import java.io.FileNotFoundException;
30import java.io.FileOutputStream;
31import java.io.IOException;
32import java.io.InputStream;
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.List;
36import java.util.Locale;
37
38/**
39 * Group class for static methods to help with creation and getting of the binary dictionary
40 * file from the dictionary provider
41 */
42public class BinaryDictionaryFileDumper {
43    private static final String TAG = BinaryDictionaryFileDumper.class.getSimpleName();
44    private static final boolean DEBUG = false;
45
46    /**
47     * The size of the temporary buffer to copy files.
48     */
49    static final int FILE_READ_BUFFER_SIZE = 1024;
50
51    private static final String DICTIONARY_PROJECTION[] = { "id" };
52
53    // Prevents this class to be accidentally instantiated.
54    private BinaryDictionaryFileDumper() {
55    }
56
57    /**
58     * Return for a given locale or dictionary id the provider URI to get the dictionary.
59     */
60    private static Uri getProviderUri(String path) {
61        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
62                .authority(BinaryDictionary.DICTIONARY_PACK_AUTHORITY).appendPath(
63                        path).build();
64    }
65
66    /**
67     * Queries a content provider for the list of word lists for a specific locale
68     * available to copy into Latin IME.
69     */
70    private static List<String> getWordListIds(final Locale locale, final Context context) {
71        final ContentResolver resolver = context.getContentResolver();
72        final Uri dictionaryPackUri = getProviderUri(locale.toString());
73
74        final Cursor c = resolver.query(dictionaryPackUri, DICTIONARY_PROJECTION, null, null, null);
75        if (null == c) return Collections.<String>emptyList();
76        if (c.getCount() <= 0 || !c.moveToFirst()) {
77            c.close();
78            return Collections.<String>emptyList();
79        }
80
81        final List<String> list = new ArrayList<String>();
82        do {
83            final String id = c.getString(0);
84            if (TextUtils.isEmpty(id)) continue;
85            list.add(id);
86        } while (c.moveToNext());
87        c.close();
88        return list;
89    }
90
91
92    /**
93     * Helper method to encapsulate exception handling.
94     */
95    private static AssetFileDescriptor openAssetFileDescriptor(final ContentResolver resolver,
96            final Uri uri) {
97        try {
98            return resolver.openAssetFileDescriptor(uri, "r");
99        } catch (FileNotFoundException e) {
100            // I don't want to log the word list URI here for security concerns
101            Log.e(TAG, "Could not find a word list from the dictionary provider.");
102            return null;
103        }
104    }
105
106    /**
107     * Caches a word list the id of which is passed as an argument. This will write the file
108     * to the cache file name designated by its id and locale, overwriting it if already present
109     * and creating it (and its containing directory) if necessary.
110     */
111    private static AssetFileAddress cacheWordList(final String id, final Locale locale,
112            final ContentResolver resolver, final Context context) {
113
114        final int COMPRESSED_CRYPTED_COMPRESSED = 0;
115        final int CRYPTED_COMPRESSED = 1;
116        final int COMPRESSED_CRYPTED = 2;
117        final int COMPRESSED_ONLY = 3;
118        final int CRYPTED_ONLY = 4;
119        final int NONE = 5;
120        final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED;
121        final int MODE_MAX = NONE;
122
123        final Uri wordListUri = getProviderUri(id);
124        final String outputFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context);
125
126        for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) {
127            InputStream originalSourceStream = null;
128            InputStream inputStream = null;
129            FileOutputStream outputStream = null;
130            AssetFileDescriptor afd = null;
131            try {
132                // Open input.
133                afd = openAssetFileDescriptor(resolver, wordListUri);
134                // If we can't open it at all, don't even try a number of times.
135                if (null == afd) return null;
136                originalSourceStream = afd.createInputStream();
137                // Open output.
138                outputStream = new FileOutputStream(outputFileName);
139                // Get the appropriate decryption method for this try
140                switch (mode) {
141                    case COMPRESSED_CRYPTED_COMPRESSED:
142                        inputStream = FileTransforms.getUncompressedStream(
143                                FileTransforms.getDecryptedStream(
144                                        FileTransforms.getUncompressedStream(
145                                                originalSourceStream)));
146                        break;
147                    case CRYPTED_COMPRESSED:
148                        inputStream = FileTransforms.getUncompressedStream(
149                                FileTransforms.getDecryptedStream(originalSourceStream));
150                        break;
151                    case COMPRESSED_CRYPTED:
152                        inputStream = FileTransforms.getDecryptedStream(
153                                FileTransforms.getUncompressedStream(originalSourceStream));
154                        break;
155                    case COMPRESSED_ONLY:
156                        inputStream = FileTransforms.getUncompressedStream(originalSourceStream);
157                        break;
158                    case CRYPTED_ONLY:
159                        inputStream = FileTransforms.getDecryptedStream(originalSourceStream);
160                        break;
161                    case NONE:
162                        inputStream = originalSourceStream;
163                        break;
164                    }
165                copyFileTo(inputStream, outputStream);
166                if (0 >= resolver.delete(wordListUri, null, null)) {
167                    Log.e(TAG, "Could not have the dictionary pack delete a word list");
168                }
169                // Success! Close files (through the finally{} clause) and return.
170                return AssetFileAddress.makeFromFileName(outputFileName);
171            } catch (Exception e) {
172                if (DEBUG) {
173                    Log.i(TAG, "Can't open word list in mode " + mode + " : " + e);
174                }
175                // Try the next method.
176            } finally {
177                // Ignore exceptions while closing files.
178                try {
179                    // afd.close() will close inputStream, we should not call inputStream.close().
180                    if (null != afd) afd.close();
181                } catch (Exception e) {
182                    Log.e(TAG, "Exception while closing a cross-process file descriptor : " + e);
183                }
184                try {
185                    if (null != outputStream) outputStream.close();
186                } catch (Exception e) {
187                    Log.e(TAG, "Exception while closing a file : " + e);
188                }
189            }
190        }
191
192        // We could not copy the file at all. This is very unexpected.
193        // I'd rather not print the word list ID to the log out of security concerns
194        Log.e(TAG, "Could not copy a word list. Will not be able to use it.");
195        // If we can't copy it we should probably delete it to avoid trying to copy it over
196        // and over each time we open LatinIME.
197        if (0 >= resolver.delete(wordListUri, null, null)) {
198            Log.e(TAG, "In addition, we were unable to delete it.");
199        }
200        return null;
201    }
202
203    /**
204     * Queries a content provider for word list data for some locale and cache the returned files
205     *
206     * This will query a content provider for word list data for a given locale, and copy the
207     * files locally so that they can be mmap'ed. This may overwrite previously cached word lists
208     * with newer versions if a newer version is made available by the content provider.
209     * @returns the addresses of the word list files, or null if no data could be obtained.
210     * @throw FileNotFoundException if the provider returns non-existent data.
211     * @throw IOException if the provider-returned data could not be read.
212     */
213    public static List<AssetFileAddress> cacheWordListsFromContentProvider(final Locale locale,
214            final Context context) {
215        final ContentResolver resolver = context.getContentResolver();
216        final List<String> idList = getWordListIds(locale, context);
217        final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>();
218        for (String id : idList) {
219            final AssetFileAddress afd = cacheWordList(id, locale, resolver, context);
220            if (null != afd) {
221                fileAddressList.add(afd);
222            }
223        }
224        return fileAddressList;
225    }
226
227    /**
228     * Copies the data in an input stream to a target file.
229     * @param input the stream to be copied.
230     * @param outputFile an outputstream to copy the data to.
231     */
232    private static void copyFileTo(final InputStream input, final FileOutputStream output)
233            throws FileNotFoundException, IOException {
234        final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE];
235        for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer))
236            output.write(buffer, 0, readBytes);
237        input.close();
238    }
239}
240