Home | History | Annotate | Download | only in latin
      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.Context;
     20 import android.content.SharedPreferences;
     21 import android.content.res.AssetFileDescriptor;
     22 import android.util.Log;
     23 
     24 import com.android.inputmethod.latin.common.LocaleUtils;
     25 import com.android.inputmethod.latin.define.DecoderSpecificConstants;
     26 import com.android.inputmethod.latin.makedict.DictionaryHeader;
     27 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
     28 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
     29 import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
     30 
     31 import java.io.File;
     32 import java.io.IOException;
     33 import java.nio.BufferUnderflowException;
     34 import java.util.ArrayList;
     35 import java.util.HashMap;
     36 import java.util.Locale;
     37 
     38 /**
     39  * Helper class to get the address of a mmap'able dictionary file.
     40  */
     41 final public class BinaryDictionaryGetter {
     42 
     43     /**
     44      * Used for Log actions from this class
     45      */
     46     private static final String TAG = BinaryDictionaryGetter.class.getSimpleName();
     47 
     48     /**
     49      * Used to return empty lists
     50      */
     51     private static final File[] EMPTY_FILE_ARRAY = new File[0];
     52 
     53     /**
     54      * Name of the common preferences name to know which word list are on and which are off.
     55      */
     56     private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs";
     57 
     58     private static final boolean SHOULD_USE_DICT_VERSION =
     59             DecoderSpecificConstants.SHOULD_USE_DICT_VERSION;
     60 
     61     // Name of the category for the main dictionary
     62     public static final String MAIN_DICTIONARY_CATEGORY = "main";
     63     public static final String ID_CATEGORY_SEPARATOR = ":";
     64 
     65     // The key considered to read the version attribute in a dictionary file.
     66     private static String VERSION_KEY = "version";
     67 
     68     // Prevents this from being instantiated
     69     private BinaryDictionaryGetter() {}
     70 
     71     /**
     72      * Generates a unique temporary file name in the app cache directory.
     73      */
     74     public static String getTempFileName(final String id, final Context context)
     75             throws IOException {
     76         final String safeId = DictionaryInfoUtils.replaceFileNameDangerousCharacters(id);
     77         final File directory = new File(DictionaryInfoUtils.getWordListTempDirectory(context));
     78         if (!directory.exists()) {
     79             if (!directory.mkdirs()) {
     80                 Log.e(TAG, "Could not create the temporary directory");
     81             }
     82         }
     83         // If the first argument is less than three chars, createTempFile throws a
     84         // RuntimeException. We don't really care about what name we get, so just
     85         // put a three-chars prefix makes us safe.
     86         return File.createTempFile("xxx" + safeId, null, directory).getAbsolutePath();
     87     }
     88 
     89     /**
     90      * Returns a file address from a resource, or null if it cannot be opened.
     91      */
     92     public static AssetFileAddress loadFallbackResource(final Context context,
     93             final int fallbackResId) {
     94         AssetFileDescriptor afd = null;
     95         try {
     96             afd = context.getResources().openRawResourceFd(fallbackResId);
     97         } catch (RuntimeException e) {
     98             Log.e(TAG, "Resource not found: " + fallbackResId);
     99             return null;
    100         }
    101         if (afd == null) {
    102             Log.e(TAG, "Resource cannot be opened: " + fallbackResId);
    103             return null;
    104         }
    105         try {
    106             return AssetFileAddress.makeFromFileNameAndOffset(
    107                     context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
    108         } finally {
    109             try {
    110                 afd.close();
    111             } catch (IOException ignored) {
    112             }
    113         }
    114     }
    115 
    116     private static final class DictPackSettings {
    117         final SharedPreferences mDictPreferences;
    118         public DictPackSettings(final Context context) {
    119             mDictPreferences = null == context ? null
    120                     : context.getSharedPreferences(COMMON_PREFERENCES_NAME,
    121                             Context.MODE_MULTI_PROCESS);
    122         }
    123         public boolean isWordListActive(final String dictId) {
    124             if (null == mDictPreferences) {
    125                 // If we don't have preferences it basically means we can't find the dictionary
    126                 // pack - either it's not installed, or it's disabled, or there is some strange
    127                 // bug. Either way, a word list with no settings should be on by default: default
    128                 // dictionaries in LatinIME are on if there is no settings at all, and if for some
    129                 // reason some dictionaries have been installed BUT the dictionary pack can't be
    130                 // found anymore it's safer to actually supply installed dictionaries.
    131                 return true;
    132             }
    133             // The default is true here for the same reasons as above. We got the dictionary
    134             // pack but if we don't have any settings for it it means the user has never been
    135             // to the settings yet. So by default, the main dictionaries should be on.
    136             return mDictPreferences.getBoolean(dictId, true);
    137         }
    138     }
    139 
    140     /**
    141      * Utility class for the {@link #getCachedWordLists} method
    142      */
    143     private static final class FileAndMatchLevel {
    144         final File mFile;
    145         final int mMatchLevel;
    146         public FileAndMatchLevel(final File file, final int matchLevel) {
    147             mFile = file;
    148             mMatchLevel = matchLevel;
    149         }
    150     }
    151 
    152     /**
    153      * Returns the list of cached files for a specific locale, one for each category.
    154      *
    155      * This will return exactly one file for each word list category that matches
    156      * the passed locale. If several files match the locale for any given category,
    157      * this returns the file with the closest match to the locale. For example, if
    158      * the passed word list is en_US, and for a category we have an en and an en_US
    159      * word list available, we'll return only the en_US one.
    160      * Thus, the list will contain as many files as there are categories.
    161      *
    162      * @param locale the locale to find the dictionary files for, as a string.
    163      * @param context the context on which to open the files upon.
    164      * @return an array of binary dictionary files, which may be empty but may not be null.
    165      */
    166     public static File[] getCachedWordLists(final String locale, final Context context) {
    167         final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context);
    168         if (null == directoryList) return EMPTY_FILE_ARRAY;
    169         final HashMap<String, FileAndMatchLevel> cacheFiles = new HashMap<>();
    170         for (File directory : directoryList) {
    171             if (!directory.isDirectory()) continue;
    172             final String dirLocale =
    173                     DictionaryInfoUtils.getWordListIdFromFileName(directory.getName());
    174             final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale);
    175             if (LocaleUtils.isMatch(matchLevel)) {
    176                 final File[] wordLists = directory.listFiles();
    177                 if (null != wordLists) {
    178                     for (File wordList : wordLists) {
    179                         final String category =
    180                                 DictionaryInfoUtils.getCategoryFromFileName(wordList.getName());
    181                         final FileAndMatchLevel currentBestMatch = cacheFiles.get(category);
    182                         if (null == currentBestMatch || currentBestMatch.mMatchLevel < matchLevel) {
    183                             cacheFiles.put(category, new FileAndMatchLevel(wordList, matchLevel));
    184                         }
    185                     }
    186                 }
    187             }
    188         }
    189         if (cacheFiles.isEmpty()) return EMPTY_FILE_ARRAY;
    190         final File[] result = new File[cacheFiles.size()];
    191         int index = 0;
    192         for (final FileAndMatchLevel entry : cacheFiles.values()) {
    193             result[index++] = entry.mFile;
    194         }
    195         return result;
    196     }
    197 
    198     // ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since
    199     // those do not include whitelist entries, the new code with an old version of the dictionary
    200     // would lose whitelist functionality.
    201     private static boolean hackCanUseDictionaryFile(final File file) {
    202         if (!SHOULD_USE_DICT_VERSION) {
    203             return true;
    204         }
    205 
    206         try {
    207             // Read the version of the file
    208             final DictionaryHeader header = BinaryDictionaryUtils.getHeader(file);
    209             final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY);
    210             if (null == version) {
    211                 // No version in the options : the format is unexpected
    212                 return false;
    213             }
    214             // Version 18 is the first one to include the whitelist
    215             // Obviously this is a big ## HACK ##
    216             return Integer.parseInt(version) >= 18;
    217         } catch (java.io.FileNotFoundException e) {
    218             return false;
    219         } catch (java.io.IOException e) {
    220             return false;
    221         } catch (NumberFormatException e) {
    222             return false;
    223         } catch (BufferUnderflowException e) {
    224             return false;
    225         } catch (UnsupportedFormatException e) {
    226             return false;
    227         }
    228     }
    229 
    230     /**
    231      * Returns a list of file addresses for a given locale, trying relevant methods in order.
    232      *
    233      * Tries to get binary dictionaries from various sources, in order:
    234      * - Uses a content provider to get a public dictionary set, as per the protocol described
    235      *   in BinaryDictionaryFileDumper.
    236      * If that fails:
    237      * - Gets a file name from the built-in dictionary for this locale, if any.
    238      * If that fails:
    239      * - Returns null.
    240      * @return The list of addresses of valid dictionary files, or null.
    241      */
    242     public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
    243             final Context context, boolean notifyDictionaryPackForUpdates) {
    244         if (notifyDictionaryPackForUpdates) {
    245             final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable(
    246                     context, locale);
    247             // It makes sure that the first time keyboard comes up and the dictionaries are reset,
    248             // the DB is populated with the appropriate values for each locale. Helps in downloading
    249             // the dictionaries when the user enables and switches new languages before the
    250             // DictionaryService runs.
    251             BinaryDictionaryFileDumper.downloadDictIfNeverRequested(
    252                     locale, context, hasDefaultWordList);
    253 
    254             // Move a staging files to the cache ddirectories if any.
    255             DictionaryInfoUtils.moveStagingFilesIfExists(context);
    256         }
    257         final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
    258         final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
    259         final DictPackSettings dictPackSettings = new DictPackSettings(context);
    260 
    261         boolean foundMainDict = false;
    262         final ArrayList<AssetFileAddress> fileList = new ArrayList<>();
    263         // cachedWordLists may not be null, see doc for getCachedDictionaryList
    264         for (final File f : cachedWordLists) {
    265             final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName());
    266             final boolean canUse = f.canRead() && hackCanUseDictionaryFile(f);
    267             if (canUse && DictionaryInfoUtils.isMainWordListId(wordListId)) {
    268                 foundMainDict = true;
    269             }
    270             if (!dictPackSettings.isWordListActive(wordListId)) continue;
    271             if (canUse) {
    272                 final AssetFileAddress afa = AssetFileAddress.makeFromFileName(f.getPath());
    273                 if (null != afa) fileList.add(afa);
    274             } else {
    275                 Log.e(TAG, "Found a cached dictionary file for " + locale.toString()
    276                         + " but cannot read or use it");
    277             }
    278         }
    279 
    280         if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
    281             final int fallbackResId =
    282                     DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale);
    283             final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId);
    284             if (null != fallbackAsset) {
    285                 fileList.add(fallbackAsset);
    286             }
    287         }
    288 
    289         return fileList;
    290     }
    291 }
    292