BinaryDictionaryGetter.java revision 0df78d46da1ef0d42196f3baa9d5f6df5932afb6
1cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard/*
2cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Copyright (C) 2011 The Android Open Source Project
3cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard *
4cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * use this file except in compliance with the License. You may obtain a copy of
6cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * the License at
7cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard *
8cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * http://www.apache.org/licenses/LICENSE-2.0
9cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard *
10cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Unless required by applicable law or agreed to in writing, software
11cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * License for the specific language governing permissions and limitations under
14cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * the License.
15cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard */
16cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard
17cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardpackage com.android.inputmethod.latin;
18cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard
19cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardimport android.content.Context;
2086e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalardimport android.content.SharedPreferences;
2186e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalardimport android.content.pm.PackageManager.NameNotFoundException;
22cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardimport android.content.res.AssetFileDescriptor;
23cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardimport android.util.Log;
24cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard
2528966734619251f78812f6a53f5efacbf5f77c49Jean Chalardimport java.io.File;
2608868624ede5eb4950972833f015d465408d3408Jean Chalardimport java.util.ArrayList;
270df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalardimport java.util.HashMap;
28cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardimport java.util.Locale;
29cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard
30cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard/**
31cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard * Helper class to get the address of a mmap'able dictionary file.
32cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard */
33cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalardclass BinaryDictionaryGetter {
34cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard
35cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard    /**
36cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     * Used for Log actions from this class
37cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     */
38cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard    private static final String TAG = BinaryDictionaryGetter.class.getSimpleName();
39cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard
4086e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    /**
4183207fb482b13bd2300008aa153080f0706fbd8dJean Chalard     * Used to return empty lists
4283207fb482b13bd2300008aa153080f0706fbd8dJean Chalard     */
4383207fb482b13bd2300008aa153080f0706fbd8dJean Chalard    private static final File[] EMPTY_FILE_ARRAY = new File[0];
4483207fb482b13bd2300008aa153080f0706fbd8dJean Chalard
4583207fb482b13bd2300008aa153080f0706fbd8dJean Chalard    /**
4686e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard     * Name of the common preferences name to know which word list are on and which are off.
4786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard     */
4886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs";
4986e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard
500df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    // Name of the category for the main dictionary
510df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    private static final String MAIN_DICTIONARY_CATEGORY = "main";
520df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    public static final String ID_CATEGORY_SEPARATOR = ":";
530df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard
54cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard    // Prevents this from being instantiated
55cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard    private BinaryDictionaryGetter() {}
56cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard
57cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard    /**
5886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard     * Returns whether we may want to use this character as part of a file name.
5986e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard     *
6086e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard     * This basically only accepts ascii letters and numbers, and rejects everything else.
6186e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard     */
6286e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    private static boolean isFileNameCharacter(int codePoint) {
6386e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard        if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
6486e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard        if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
6586e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard        if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
6686e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard        return codePoint == '_'; // Underscore
6786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    }
6886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard
6986e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    /**
7028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * Escapes a string for any characters that may be suspicious for a file or directory name.
7128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     *
7228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * Concretely this does a sort of URL-encoding except it will encode everything that's not
7328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
7428966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * we cannot allow here)
7528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     */
7628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard    // TODO: create a unit test for this method
7786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    private static String replaceFileNameDangerousCharacters(final String name) {
7828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        // This assumes '%' is fully available as a non-separator, normal
7928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        // character in a file name. This is probably true for all file systems.
8028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        final StringBuilder sb = new StringBuilder();
819242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard        final int nameLength = name.length();
829242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard        for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
8328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard            final int codePoint = name.codePointAt(i);
8486e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard            if (isFileNameCharacter(codePoint)) {
8528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard                sb.appendCodePoint(codePoint);
8628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard            } else {
8786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard                // 6 digits - unicode is limited to 21 bits
8886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard                sb.append(String.format((Locale)null, "%%%1$06x", codePoint));
8986e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard            }
9086e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard        }
9186e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard        return sb.toString();
9286e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    }
9386e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard
9486e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    /**
9586e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard     * Reverse escaping done by replaceFileNameDangerousCharacters.
9686e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard     */
9786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard    private static String getWordListIdFromFileName(final String fname) {
9886e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard        final StringBuilder sb = new StringBuilder();
999242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard        final int fnameLength = fname.length();
1009242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard        for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
10186e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard            final int codePoint = fname.codePointAt(i);
10286e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard            if ('%' != codePoint) {
10386e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard                sb.appendCodePoint(codePoint);
10486e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard            } else {
10586e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard                final int encodedCodePoint = Integer.parseInt(fname.substring(i + 1, i + 7), 16);
10686e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard                i += 6;
10786e517fe4a5981f6ab936a0f9f40a0e0aa196477Jean Chalard                sb.appendCodePoint(encodedCodePoint);
10828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard            }
10928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        }
11028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        return sb.toString();
11128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard    }
11228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard
11328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard    /**
114de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * Helper method to get the top level cache directory.
115de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     */
116de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    private static String getWordListCacheDirectory(final Context context) {
117de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        return context.getFilesDir() + File.separator + "dicts";
118de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    }
119de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard
120de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    /**
12128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * Find out the cache directory associated with a specific locale.
12228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     */
123de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    private static String getCacheDirectoryForLocale(final String locale, final Context context) {
124de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
125de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
12628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard                + relativeDirectoryName;
12728966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        final File directory = new File(absoluteDirectoryName);
12828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        if (!directory.exists()) {
12928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard            if (!directory.mkdirs()) {
13028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard                Log.e(TAG, "Could not create the directory for locale" + locale);
13128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard            }
13228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        }
13328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        return absoluteDirectoryName;
13428966734619251f78812f6a53f5efacbf5f77c49Jean Chalard    }
13528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard
13628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard    /**
13728966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * Generates a file name for the id and locale passed as an argument.
13828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     *
13928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * In the current implementation the file name returned will always be unique for
14028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * any id/locale pair, but please do not expect that the id can be the same for
14128966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * different dictionaries with different locales. An id should be unique for any
14228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * dictionary.
14328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * The file name is pretty much an URL-encoded version of the id inside a directory
14428966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * named like the locale, except it will also escape characters that look dangerous
14528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * to some file systems.
14628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * @param id the id of the dictionary for which to get a file name
147de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * @param locale the locale for which to get the file name as a string
14828966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * @param context the context to use for getting the directory
14928966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     * @return the name of the file to be created
15028966734619251f78812f6a53f5efacbf5f77c49Jean Chalard     */
151de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    public static String getCacheFileName(String id, String locale, Context context) {
15228966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        final String fileName = replaceFileNameDangerousCharacters(id);
15328966734619251f78812f6a53f5efacbf5f77c49Jean Chalard        return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
15428966734619251f78812f6a53f5efacbf5f77c49Jean Chalard    }
15528966734619251f78812f6a53f5efacbf5f77c49Jean Chalard
15628966734619251f78812f6a53f5efacbf5f77c49Jean Chalard    /**
157cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     * Returns a file address from a resource, or null if it cannot be opened.
158cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     */
159e150ef98569d61078e0f8c67ded8364a9c3d4a20Jean Chalard    private static AssetFileAddress loadFallbackResource(final Context context,
16078ab80844b4f8e0369f4e86b2a02208197f9bd34Tadashi G. Takaoka            final int fallbackResId) {
16178ab80844b4f8e0369f4e86b2a02208197f9bd34Tadashi G. Takaoka        final AssetFileDescriptor afd = context.getResources().openRawResourceFd(fallbackResId);
162cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard        if (afd == null) {
163cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard            Log.e(TAG, "Found the resource but cannot read it. Is it compressed? resId="
164cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard                    + fallbackResId);
165cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard            return null;
166cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard        }
167cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard        return AssetFileAddress.makeFromFileNameAndOffset(
168cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard                context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
169cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard    }
170cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard
171c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard    static private class DictPackSettings {
172c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard        final SharedPreferences mDictPreferences;
173c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard        public DictPackSettings(final Context context) {
174c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard            Context dictPackContext = null;
175c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard            try {
176c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                final String dictPackName =
177c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                        context.getString(R.string.dictionary_pack_package_name);
178c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                dictPackContext = context.createPackageContext(dictPackName, 0);
179c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard            } catch (NameNotFoundException e) {
180c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // The dictionary pack is not installed...
181c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // TODO: fallback on the built-in dict, see the TODO above
182c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                Log.e(TAG, "Could not find a dictionary pack");
183c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard            }
184c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard            mDictPreferences = null == dictPackContext ? null
185c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                    : dictPackContext.getSharedPreferences(COMMON_PREFERENCES_NAME,
186c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                            Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS);
187c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard        }
188c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard        public boolean isWordListActive(final String dictId) {
189c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard            if (null == mDictPreferences) {
190c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // If we don't have preferences it basically means we can't find the dictionary
191c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // pack - either it's not installed, or it's disabled, or there is some strange
192c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // bug. Either way, a word list with no settings should be on by default: default
193c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // dictionaries in LatinIME are on if there is no settings at all, and if for some
194c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // reason some dictionaries have been installed BUT the dictionary pack can't be
195c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // found anymore it's safer to actually supply installed dictionaries.
196c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                return true;
197c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard            } else {
198c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // The default is true here for the same reasons as above. We got the dictionary
199c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // pack but if we don't have any settings for it it means the user has never been
200c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                // to the settings yet. So by default, the main dictionaries should be on.
201c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard                return mDictPreferences.getBoolean(dictId, true);
202c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard            }
203c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard        }
204c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard    }
205c11c4fd61b3574f3647299ec0f19ee01ecaabf52Jean Chalard
206cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard    /**
207de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * Helper method to the list of cache directories, one for each distinct locale.
208de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     */
209de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    private static File[] getCachedDirectoryList(final Context context) {
210de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        return new File(getWordListCacheDirectory(context)).listFiles();
211de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    }
212de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard
213de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    /**
2140df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * Returns the category for a given file name.
2150df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     *
2160df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * This parses the file name, extracts the category, and returns it. See
2170df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}.
2180df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * @return The category as a string or null if it can't be found in the file name.
2190df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     */
2200df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    private static String getCategoryFromFileName(final String fileName) {
2210df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        final String id = getWordListIdFromFileName(fileName);
2220df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        final String[] idArray = id.split(ID_CATEGORY_SEPARATOR);
2230df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        if (2 != idArray.length) return null;
2240df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        return idArray[0];
2250df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    }
2260df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard
2270df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    /**
2280df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * Utility class for the {@link #getCachedWordLists} method
2290df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     */
2300df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    private static class FileAndMatchLevel {
2310df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        final File mFile;
2320df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        final int mMatchLevel;
2330df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        public FileAndMatchLevel(final File file, final int matchLevel) {
2340df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard            mFile = file;
2350df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard            mMatchLevel = matchLevel;
2360df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        }
2370df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    }
2380df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard
2390df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    /**
2400df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * Returns the list of cached files for a specific locale, one for each category.
2410df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     *
2420df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * This will return exactly one file for each word list category that matches
2430df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * the passed locale. If several files match the locale for any given category,
2440df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * this returns the file with the closest match to the locale. For example, if
2450df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * the passed word list is en_US, and for a category we have an en and an en_US
2460df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * word list available, we'll return only the en_US one.
2470df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard     * Thus, the list will contain as many files as there are categories.
24808868624ede5eb4950972833f015d465408d3408Jean Chalard     *
249de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * @param locale the locale to find the dictionary files for, as a string.
25008868624ede5eb4950972833f015d465408d3408Jean Chalard     * @param context the context on which to open the files upon.
25183207fb482b13bd2300008aa153080f0706fbd8dJean Chalard     * @return an array of binary dictionary files, which may be empty but may not be null.
25208868624ede5eb4950972833f015d465408d3408Jean Chalard     */
253de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard    private static File[] getCachedWordLists(final String locale,
25408868624ede5eb4950972833f015d465408d3408Jean Chalard            final Context context) {
255de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        final File[] directoryList = getCachedDirectoryList(context);
256de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        if (null == directoryList) return EMPTY_FILE_ARRAY;
2570df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        final HashMap<String, FileAndMatchLevel> cacheFiles =
2580df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard                new HashMap<String, FileAndMatchLevel>();
259de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        for (File directory : directoryList) {
260de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard            if (!directory.isDirectory()) continue;
261de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard            final String dirLocale = getWordListIdFromFileName(directory.getName());
2620df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard            final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale);
2630df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard            if (LocaleUtils.isMatch(matchLevel)) {
264de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard                final File[] wordLists = directory.listFiles();
265de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard                if (null != wordLists) {
266de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard                    for (File wordList : wordLists) {
2670df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard                        final String category = getCategoryFromFileName(wordList.getName());
2680df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard                        final FileAndMatchLevel currentBestMatch = cacheFiles.get(category);
2690df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard                        if (null == currentBestMatch || currentBestMatch.mMatchLevel < matchLevel) {
2700df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard                            cacheFiles.put(category, new FileAndMatchLevel(wordList, matchLevel));
2710df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard                        }
272de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard                    }
273de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard                }
274de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard            }
275de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        }
276de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        if (cacheFiles.isEmpty()) return EMPTY_FILE_ARRAY;
2770df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        final File[] result = new File[cacheFiles.size()];
2780df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        int index = 0;
2790df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        for (final FileAndMatchLevel entry : cacheFiles.values()) {
2800df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard            result[index++] = entry.mFile;
2810df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        }
2820df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        return result;
28308868624ede5eb4950972833f015d465408d3408Jean Chalard    }
28408868624ede5eb4950972833f015d465408d3408Jean Chalard
28508868624ede5eb4950972833f015d465408d3408Jean Chalard    /**
286de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * Returns the id associated with the main word list for a specified locale.
287de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     *
288de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * Word lists stored in Android Keyboard's resources are referred to as the "main"
289de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * word lists. Since they can be updated like any other list, we need to assign a
290de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * unique ID to them. This ID is just the name of the language (locale-wise) they
291de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard     * are for, and this method returns this ID.
292ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard     */
293ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard    private static String getMainDictId(final Locale locale) {
294de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        // This works because we don't include by default different dictionaries for
295de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        // different countries. This actually needs to return the id that we would
296de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        // like to use for word lists included in resources, and the following is okay.
2970df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        return MAIN_DICTIONARY_CATEGORY + ID_CATEGORY_SEPARATOR + locale.getLanguage().toString();
2980df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    }
2990df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard
3000df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard    private static boolean isMainWordListId(final String id) {
3010df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        final String[] idArray = id.split(ID_CATEGORY_SEPARATOR);
3020df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        if (2 != idArray.length) return false;
3030df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard        return MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
304ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard    }
305ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard
306ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard    /**
307d8f52a4f18d22aa150846b01017410ce70bbad6fJean Chalard     * Returns a list of file addresses for a given locale, trying relevant methods in order.
308cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     *
309d8f52a4f18d22aa150846b01017410ce70bbad6fJean Chalard     * Tries to get binary dictionaries from various sources, in order:
310d8f52a4f18d22aa150846b01017410ce70bbad6fJean Chalard     * - Uses a content provider to get a public dictionary set, as per the protocol described
311cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     *   in BinaryDictionaryFileDumper.
312cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     * If that fails:
313e6269759d642eac0a03ae6942acb5cd556e7ff46Jean Chalard     * - Gets a file name from the built-in dictionary for this locale, if any.
314cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     * If that fails:
315cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     * - Returns null.
316660776e09b9a3b321074a94721d901a035ca1b9fKen Wakasa     * @return The list of addresses of valid dictionary files, or null.
317cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard     */
318660776e09b9a3b321074a94721d901a035ca1b9fKen Wakasa    public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
319e6269759d642eac0a03ae6942acb5cd556e7ff46Jean Chalard            final Context context) {
32080e0bf04292867ddc769aca75ebaee817b95a941Jean Chalard
321cec8552b18fd74517512a43a8d75f64e64bd12c3Jean Chalard        final boolean hasDefaultWordList = DictionaryFactory.isDictionaryAvailable(context, locale);
3227b1f74bb9ddae952f4da6c8d9bbb0057984b0988Jean Chalard        // cacheWordListsFromContentProvider returns the list of files it copied to local
32380e0bf04292867ddc769aca75ebaee817b95a941Jean Chalard        // storage, but we don't really care about what was copied NOW: what we want is the
32480e0bf04292867ddc769aca75ebaee817b95a941Jean Chalard        // list of everything we ever cached, so we ignore the return value.
325cec8552b18fd74517512a43a8d75f64e64bd12c3Jean Chalard        BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context,
326cec8552b18fd74517512a43a8d75f64e64bd12c3Jean Chalard                hasDefaultWordList);
327de4e8dedccc7b6db6df4c3f75d9f2458432c558aJean Chalard        final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
328ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard        final String mainDictId = getMainDictId(locale);
32983207fb482b13bd2300008aa153080f0706fbd8dJean Chalard        final DictPackSettings dictPackSettings = new DictPackSettings(context);
33083207fb482b13bd2300008aa153080f0706fbd8dJean Chalard
331ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard        boolean foundMainDict = false;
33283207fb482b13bd2300008aa153080f0706fbd8dJean Chalard        final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>();
3337b1f74bb9ddae952f4da6c8d9bbb0057984b0988Jean Chalard        // cachedWordLists may not be null, see doc for getCachedDictionaryList
3347b1f74bb9ddae952f4da6c8d9bbb0057984b0988Jean Chalard        for (final File f : cachedWordLists) {
33583207fb482b13bd2300008aa153080f0706fbd8dJean Chalard            final String wordListId = getWordListIdFromFileName(f.getName());
3360df78d46da1ef0d42196f3baa9d5f6df5932afb6Jean Chalard            if (isMainWordListId(wordListId)) {
337ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard                foundMainDict = true;
338ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard            }
33983207fb482b13bd2300008aa153080f0706fbd8dJean Chalard            if (!dictPackSettings.isWordListActive(wordListId)) continue;
34083207fb482b13bd2300008aa153080f0706fbd8dJean Chalard            if (f.canRead()) {
34183207fb482b13bd2300008aa153080f0706fbd8dJean Chalard                fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
34283207fb482b13bd2300008aa153080f0706fbd8dJean Chalard            } else {
34383207fb482b13bd2300008aa153080f0706fbd8dJean Chalard                Log.e(TAG, "Found a cached dictionary file but cannot read it");
34483207fb482b13bd2300008aa153080f0706fbd8dJean Chalard            }
34583207fb482b13bd2300008aa153080f0706fbd8dJean Chalard        }
34683207fb482b13bd2300008aa153080f0706fbd8dJean Chalard
347ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard        if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
348e6269759d642eac0a03ae6942acb5cd556e7ff46Jean Chalard            final int fallbackResId =
349e6269759d642eac0a03ae6942acb5cd556e7ff46Jean Chalard                    DictionaryFactory.getMainDictionaryResourceId(context.getResources(), locale);
35078ab80844b4f8e0369f4e86b2a02208197f9bd34Tadashi G. Takaoka            final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId);
351ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard            if (null != fallbackAsset) {
352ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard                fileList.add(fallbackAsset);
353ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard            }
354cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard        }
35580e0bf04292867ddc769aca75ebaee817b95a941Jean Chalard
356ee7daefd972979898d91974ea0d92fcc9f3ca169Jean Chalard        return fileList;
357cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard    }
358cba93f50c3d46ada773ec49435689dc3e2094385Jean Chalard}
359