Home | History | Annotate | Download | only in utils
      1 /*
      2  * Copyright (C) 2013 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.utils;
     18 
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.content.res.AssetManager;
     22 import android.content.res.Resources;
     23 import android.text.TextUtils;
     24 import android.util.Log;
     25 import android.view.inputmethod.InputMethodSubtype;
     26 
     27 import com.android.inputmethod.annotations.UsedForTesting;
     28 import com.android.inputmethod.dictionarypack.UpdateHandler;
     29 import com.android.inputmethod.latin.AssetFileAddress;
     30 import com.android.inputmethod.latin.BinaryDictionaryGetter;
     31 import com.android.inputmethod.latin.R;
     32 import com.android.inputmethod.latin.RichInputMethodManager;
     33 import com.android.inputmethod.latin.common.FileUtils;
     34 import com.android.inputmethod.latin.common.LocaleUtils;
     35 import com.android.inputmethod.latin.define.DecoderSpecificConstants;
     36 import com.android.inputmethod.latin.makedict.DictionaryHeader;
     37 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
     38 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
     39 
     40 import java.io.File;
     41 import java.io.FilenameFilter;
     42 import java.io.IOException;
     43 import java.util.ArrayList;
     44 import java.util.Iterator;
     45 import java.util.List;
     46 import java.util.Locale;
     47 import java.util.concurrent.TimeUnit;
     48 
     49 import javax.annotation.Nonnull;
     50 import javax.annotation.Nullable;
     51 
     52 /**
     53  * This class encapsulates the logic for the Latin-IME side of dictionary information management.
     54  */
     55 public class DictionaryInfoUtils {
     56     private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
     57     public static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName();
     58     private static final String DEFAULT_MAIN_DICT = "main";
     59     private static final String MAIN_DICT_PREFIX = "main_";
     60     private static final String DECODER_DICT_SUFFIX = DecoderSpecificConstants.DECODER_DICT_SUFFIX;
     61     // 6 digits - unicode is limited to 21 bits
     62     private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
     63 
     64     private static final String TEMP_DICT_FILE_SUB = UpdateHandler.TEMP_DICT_FILE_SUB;
     65 
     66     public static class DictionaryInfo {
     67         private static final String LOCALE_COLUMN = "locale";
     68         private static final String WORDLISTID_COLUMN = "id";
     69         private static final String LOCAL_FILENAME_COLUMN = "filename";
     70         private static final String DESCRIPTION_COLUMN = "description";
     71         private static final String DATE_COLUMN = "date";
     72         private static final String FILESIZE_COLUMN = "filesize";
     73         private static final String VERSION_COLUMN = "version";
     74 
     75         @Nonnull public final String mId;
     76         @Nonnull public final Locale mLocale;
     77         @Nullable public final String mDescription;
     78         @Nullable public final String mFilename;
     79         public final long mFilesize;
     80         public final long mModifiedTimeMillis;
     81         public final int mVersion;
     82 
     83         public DictionaryInfo(@Nonnull String id, @Nonnull Locale locale,
     84                 @Nullable String description, @Nullable String filename,
     85                 long filesize, long modifiedTimeMillis, int version) {
     86             mId = id;
     87             mLocale = locale;
     88             mDescription = description;
     89             mFilename = filename;
     90             mFilesize = filesize;
     91             mModifiedTimeMillis = modifiedTimeMillis;
     92             mVersion = version;
     93         }
     94 
     95         public ContentValues toContentValues() {
     96             final ContentValues values = new ContentValues();
     97             values.put(WORDLISTID_COLUMN, mId);
     98             values.put(LOCALE_COLUMN, mLocale.toString());
     99             values.put(DESCRIPTION_COLUMN, mDescription);
    100             values.put(LOCAL_FILENAME_COLUMN, mFilename != null ? mFilename : "");
    101             values.put(DATE_COLUMN, TimeUnit.MILLISECONDS.toSeconds(mModifiedTimeMillis));
    102             values.put(FILESIZE_COLUMN, mFilesize);
    103             values.put(VERSION_COLUMN, mVersion);
    104             return values;
    105         }
    106 
    107         @Override
    108         public String toString() {
    109             return "DictionaryInfo : Id = '" + mId
    110                     + "' : Locale=" + mLocale
    111                     + " : Version=" + mVersion;
    112         }
    113     }
    114 
    115     private DictionaryInfoUtils() {
    116         // Private constructor to forbid instantation of this helper class.
    117     }
    118 
    119     /**
    120      * Returns whether we may want to use this character as part of a file name.
    121      *
    122      * This basically only accepts ascii letters and numbers, and rejects everything else.
    123      */
    124     private static boolean isFileNameCharacter(int codePoint) {
    125         if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
    126         if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
    127         if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
    128         return codePoint == '_'; // Underscore
    129     }
    130 
    131     /**
    132      * Escapes a string for any characters that may be suspicious for a file or directory name.
    133      *
    134      * Concretely this does a sort of URL-encoding except it will encode everything that's not
    135      * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
    136      * we cannot allow here)
    137      */
    138     // TODO: create a unit test for this method
    139     public static String replaceFileNameDangerousCharacters(final String name) {
    140         // This assumes '%' is fully available as a non-separator, normal
    141         // character in a file name. This is probably true for all file systems.
    142         final StringBuilder sb = new StringBuilder();
    143         final int nameLength = name.length();
    144         for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
    145             final int codePoint = name.codePointAt(i);
    146             if (DictionaryInfoUtils.isFileNameCharacter(codePoint)) {
    147                 sb.appendCodePoint(codePoint);
    148             } else {
    149                 sb.append(String.format((Locale)null, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x",
    150                         codePoint));
    151             }
    152         }
    153         return sb.toString();
    154     }
    155 
    156     /**
    157      * Helper method to get the top level cache directory.
    158      */
    159     private static String getWordListCacheDirectory(final Context context) {
    160         return context.getFilesDir() + File.separator + "dicts";
    161     }
    162 
    163     /**
    164      * Helper method to get the top level cache directory.
    165      */
    166     public static String getWordListStagingDirectory(final Context context) {
    167         return context.getFilesDir() + File.separator + "staging";
    168     }
    169 
    170     /**
    171      * Helper method to get the top level temp directory.
    172      */
    173     public static String getWordListTempDirectory(final Context context) {
    174         return context.getFilesDir() + File.separator + "tmp";
    175     }
    176 
    177     /**
    178      * Reverse escaping done by {@link #replaceFileNameDangerousCharacters(String)}.
    179      */
    180     @Nonnull
    181     public static String getWordListIdFromFileName(@Nonnull final String fname) {
    182         final StringBuilder sb = new StringBuilder();
    183         final int fnameLength = fname.length();
    184         for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
    185             final int codePoint = fname.codePointAt(i);
    186             if ('%' != codePoint) {
    187                 sb.appendCodePoint(codePoint);
    188             } else {
    189                 // + 1 to pass the % sign
    190                 final int encodedCodePoint = Integer.parseInt(
    191                         fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT), 16);
    192                 i += MAX_HEX_DIGITS_FOR_CODEPOINT;
    193                 sb.appendCodePoint(encodedCodePoint);
    194             }
    195         }
    196         return sb.toString();
    197     }
    198 
    199     /**
    200      * Helper method to the list of cache directories, one for each distinct locale.
    201      */
    202     public static File[] getCachedDirectoryList(final Context context) {
    203         return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
    204     }
    205 
    206     public static File[] getStagingDirectoryList(final Context context) {
    207         return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles();
    208     }
    209 
    210     @Nullable
    211     public static File[] getUnusedDictionaryList(final Context context) {
    212         return context.getFilesDir().listFiles(new FilenameFilter() {
    213             @Override
    214             public boolean accept(File dir, String filename) {
    215                 return !TextUtils.isEmpty(filename) && filename.endsWith(".dict")
    216                         && filename.contains(TEMP_DICT_FILE_SUB);
    217             }
    218         });
    219     }
    220 
    221     /**
    222      * Returns the category for a given file name.
    223      *
    224      * This parses the file name, extracts the category, and returns it. See
    225      * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}.
    226      * @return The category as a string or null if it can't be found in the file name.
    227      */
    228     @Nullable
    229     public static String getCategoryFromFileName(@Nonnull final String fileName) {
    230         final String id = getWordListIdFromFileName(fileName);
    231         final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
    232         // An id is supposed to be in format category:locale, so splitting on the separator
    233         // should yield a 2-elements array
    234         if (2 != idArray.length) {
    235             return null;
    236         }
    237         return idArray[0];
    238     }
    239 
    240     /**
    241      * Find out the cache directory associated with a specific locale.
    242      */
    243     public static String getCacheDirectoryForLocale(final String locale, final Context context) {
    244         final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
    245         final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
    246                 + relativeDirectoryName;
    247         final File directory = new File(absoluteDirectoryName);
    248         if (!directory.exists()) {
    249             if (!directory.mkdirs()) {
    250                 Log.e(TAG, "Could not create the directory for locale" + locale);
    251             }
    252         }
    253         return absoluteDirectoryName;
    254     }
    255 
    256     /**
    257      * Generates a file name for the id and locale passed as an argument.
    258      *
    259      * In the current implementation the file name returned will always be unique for
    260      * any id/locale pair, but please do not expect that the id can be the same for
    261      * different dictionaries with different locales. An id should be unique for any
    262      * dictionary.
    263      * The file name is pretty much an URL-encoded version of the id inside a directory
    264      * named like the locale, except it will also escape characters that look dangerous
    265      * to some file systems.
    266      * @param id the id of the dictionary for which to get a file name
    267      * @param locale the locale for which to get the file name as a string
    268      * @param context the context to use for getting the directory
    269      * @return the name of the file to be created
    270      */
    271     public static String getCacheFileName(String id, String locale, Context context) {
    272         final String fileName = replaceFileNameDangerousCharacters(id);
    273         return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
    274     }
    275 
    276     public static String getStagingFileName(String id, String locale, Context context) {
    277         final String stagingDirectory = getWordListStagingDirectory(context);
    278         // create the directory if it does not exist.
    279         final File directory = new File(stagingDirectory);
    280         if (!directory.exists()) {
    281             if (!directory.mkdirs()) {
    282                 Log.e(TAG, "Could not create the staging directory.");
    283             }
    284         }
    285         // e.g. id="main:en_in", locale ="en_IN"
    286         final String fileName = replaceFileNameDangerousCharacters(
    287                 locale + TEMP_DICT_FILE_SUB + id);
    288         return stagingDirectory + File.separator + fileName;
    289     }
    290 
    291     public static void moveStagingFilesIfExists(Context context) {
    292         final File[] stagingFiles = DictionaryInfoUtils.getStagingDirectoryList(context);
    293         if (stagingFiles != null && stagingFiles.length > 0) {
    294             for (final File stagingFile : stagingFiles) {
    295                 final String fileName = stagingFile.getName();
    296                 final int index = fileName.indexOf(TEMP_DICT_FILE_SUB);
    297                 if (index == -1) {
    298                     // This should never happen.
    299                     Log.e(TAG, "Staging file does not have ___ substring.");
    300                     continue;
    301                 }
    302                 final String[] localeAndFileId = fileName.split(TEMP_DICT_FILE_SUB);
    303                 if (localeAndFileId.length != 2) {
    304                     Log.e(TAG, String.format("malformed staging file %s. Deleting.",
    305                             stagingFile.getAbsoluteFile()));
    306                     stagingFile.delete();
    307                     continue;
    308                 }
    309 
    310                 final String locale = localeAndFileId[0];
    311                 // already escaped while moving to staging.
    312                 final String fileId = localeAndFileId[1];
    313                 final String cacheDirectoryForLocale = getCacheDirectoryForLocale(locale, context);
    314                 final String cacheFilename = cacheDirectoryForLocale + File.separator + fileId;
    315                 final File cacheFile = new File(cacheFilename);
    316                 // move the staging file to cache file.
    317                 if (!FileUtils.renameTo(stagingFile, cacheFile)) {
    318                     Log.e(TAG, String.format("Failed to rename from %s to %s.",
    319                             stagingFile.getAbsoluteFile(), cacheFile.getAbsoluteFile()));
    320                 }
    321             }
    322         }
    323     }
    324 
    325     public static boolean isMainWordListId(final String id) {
    326         final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
    327         // An id is supposed to be in format category:locale, so splitting on the separator
    328         // should yield a 2-elements array
    329         if (2 != idArray.length) {
    330             return false;
    331         }
    332         return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
    333     }
    334 
    335     /**
    336      * Find out whether a dictionary is available for this locale.
    337      * @param context the context on which to check resources.
    338      * @param locale the locale to check for.
    339      * @return whether a (non-placeholder) dictionary is available or not.
    340      */
    341     public static boolean isDictionaryAvailable(final Context context, final Locale locale) {
    342         final Resources res = context.getResources();
    343         return 0 != getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
    344     }
    345 
    346     /**
    347      * Helper method to return a dictionary res id for a locale, or 0 if none.
    348      * @param res resources for the app
    349      * @param locale dictionary locale
    350      * @return main dictionary resource id
    351      */
    352     public static int getMainDictionaryResourceIdIfAvailableForLocale(final Resources res,
    353             final Locale locale) {
    354         int resId;
    355         // Try to find main_language_country dictionary.
    356         if (!locale.getCountry().isEmpty()) {
    357             final String dictLanguageCountry = MAIN_DICT_PREFIX
    358                     + locale.toString().toLowerCase(Locale.ROOT) + DECODER_DICT_SUFFIX;
    359             if ((resId = res.getIdentifier(
    360                     dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
    361                 return resId;
    362             }
    363         }
    364 
    365         // Try to find main_language dictionary.
    366         final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage() + DECODER_DICT_SUFFIX;
    367         if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
    368             return resId;
    369         }
    370 
    371         // Not found, return 0
    372         return 0;
    373     }
    374 
    375     /**
    376      * Returns a main dictionary resource id
    377      * @param res resources for the app
    378      * @param locale dictionary locale
    379      * @return main dictionary resource id
    380      */
    381     public static int getMainDictionaryResourceId(final Resources res, final Locale locale) {
    382         int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
    383         if (0 != resourceId) {
    384             return resourceId;
    385         }
    386         return res.getIdentifier(DEFAULT_MAIN_DICT + DecoderSpecificConstants.DECODER_DICT_SUFFIX,
    387                 "raw", RESOURCE_PACKAGE_NAME);
    388     }
    389 
    390     /**
    391      * Returns the id associated with the main word list for a specified locale.
    392      *
    393      * Word lists stored in Android Keyboard's resources are referred to as the "main"
    394      * word lists. Since they can be updated like any other list, we need to assign a
    395      * unique ID to them. This ID is just the name of the language (locale-wise) they
    396      * are for, and this method returns this ID.
    397      */
    398     public static String getMainDictId(@Nonnull final Locale locale) {
    399         // This works because we don't include by default different dictionaries for
    400         // different countries. This actually needs to return the id that we would
    401         // like to use for word lists included in resources, and the following is okay.
    402         return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY +
    403                 BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.toString().toLowerCase();
    404     }
    405 
    406     public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file,
    407             final long offset, final long length) {
    408         try {
    409             final DictionaryHeader header =
    410                     BinaryDictionaryUtils.getHeaderWithOffsetAndLength(file, offset, length);
    411             return header;
    412         } catch (UnsupportedFormatException e) {
    413             return null;
    414         } catch (IOException e) {
    415             return null;
    416         }
    417     }
    418 
    419     /**
    420      * Returns information of the dictionary.
    421      *
    422      * @param fileAddress the asset dictionary file address.
    423      * @param locale Locale for this file.
    424      * @return information of the specified dictionary.
    425      */
    426     private static DictionaryInfo createDictionaryInfoFromFileAddress(
    427             @Nonnull final AssetFileAddress fileAddress, final Locale locale) {
    428         final String id = getMainDictId(locale);
    429         final int version = DictionaryHeaderUtils.getContentVersion(fileAddress);
    430         final String description = SubtypeLocaleUtils
    431                 .getSubtypeLocaleDisplayName(locale.toString());
    432         // Do not store the filename on db as it will try to move the filename from db to the
    433         // cached directory. If the filename is already in cached directory, this is not
    434         // necessary.
    435         final String filenameToStoreOnDb = null;
    436         return new DictionaryInfo(id, locale, description, filenameToStoreOnDb,
    437                 fileAddress.mLength, new File(fileAddress.mFilename).lastModified(), version);
    438     }
    439 
    440     /**
    441      * Returns the information of the dictionary for the given {@link AssetFileAddress}.
    442      * If the file is corrupted or a pre-fava file, then the file gets deleted and the null
    443      * value is returned.
    444      */
    445     @Nullable
    446     private static DictionaryInfo createDictionaryInfoForUnCachedFile(
    447             @Nonnull final AssetFileAddress fileAddress, final Locale locale) {
    448         final String id = getMainDictId(locale);
    449         final int version = DictionaryHeaderUtils.getContentVersion(fileAddress);
    450 
    451         if (version == -1) {
    452             // Purge the pre-fava/corrupted unused dictionaires.
    453             fileAddress.deleteUnderlyingFile();
    454             return null;
    455         }
    456 
    457         final String description = SubtypeLocaleUtils
    458                 .getSubtypeLocaleDisplayName(locale.toString());
    459 
    460         final File unCachedFile = new File(fileAddress.mFilename);
    461         // Store just the filename and not the full path.
    462         final String filenameToStoreOnDb = unCachedFile.getName();
    463         return new DictionaryInfo(id, locale, description, filenameToStoreOnDb, fileAddress.mLength,
    464                 unCachedFile.lastModified(), version);
    465     }
    466 
    467     /**
    468      * Returns dictionary information for the given locale.
    469      */
    470     private static DictionaryInfo createDictionaryInfoFromLocale(Locale locale) {
    471         final String id = getMainDictId(locale);
    472         final int version = -1;
    473         final String description = SubtypeLocaleUtils
    474                 .getSubtypeLocaleDisplayName(locale.toString());
    475         return new DictionaryInfo(id, locale, description, null, 0L, 0L, version);
    476     }
    477 
    478     private static void addOrUpdateDictInfo(final ArrayList<DictionaryInfo> dictList,
    479             final DictionaryInfo newElement) {
    480         final Iterator<DictionaryInfo> iter = dictList.iterator();
    481         while (iter.hasNext()) {
    482             final DictionaryInfo thisDictInfo = iter.next();
    483             if (thisDictInfo.mLocale.equals(newElement.mLocale)) {
    484                 if (newElement.mVersion <= thisDictInfo.mVersion) {
    485                     return;
    486                 }
    487                 iter.remove();
    488             }
    489         }
    490         dictList.add(newElement);
    491     }
    492 
    493     public static ArrayList<DictionaryInfo> getCurrentDictionaryFileNameAndVersionInfo(
    494             final Context context) {
    495         final ArrayList<DictionaryInfo> dictList = new ArrayList<>();
    496 
    497         // Retrieve downloaded dictionaries from cached directories
    498         final File[] directoryList = getCachedDirectoryList(context);
    499         if (null != directoryList) {
    500             for (final File directory : directoryList) {
    501                 final String localeString = getWordListIdFromFileName(directory.getName());
    502                 final File[] dicts = BinaryDictionaryGetter.getCachedWordLists(
    503                         localeString, context);
    504                 for (final File dict : dicts) {
    505                     final String wordListId = getWordListIdFromFileName(dict.getName());
    506                     if (!DictionaryInfoUtils.isMainWordListId(wordListId)) {
    507                         continue;
    508                     }
    509                     final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
    510                     final AssetFileAddress fileAddress = AssetFileAddress.makeFromFile(dict);
    511                     final DictionaryInfo dictionaryInfo =
    512                             createDictionaryInfoFromFileAddress(fileAddress, locale);
    513                     // Protect against cases of a less-specific dictionary being found, like an
    514                     // en dictionary being used for an en_US locale. In this case, the en dictionary
    515                     // should be used for en_US but discounted for listing purposes.
    516                     if (dictionaryInfo == null || !dictionaryInfo.mLocale.equals(locale)) {
    517                         continue;
    518                     }
    519                     addOrUpdateDictInfo(dictList, dictionaryInfo);
    520                 }
    521             }
    522         }
    523 
    524         // Retrieve downloaded dictionaries from the unused dictionaries.
    525         File[] unusedDictionaryList = getUnusedDictionaryList(context);
    526         if (unusedDictionaryList != null) {
    527             for (File dictionaryFile : unusedDictionaryList) {
    528                 String fileName = dictionaryFile.getName();
    529                 int index = fileName.indexOf(TEMP_DICT_FILE_SUB);
    530                 if (index == -1) {
    531                     continue;
    532                 }
    533                 String locale = fileName.substring(0, index);
    534                 DictionaryInfo dictionaryInfo = createDictionaryInfoForUnCachedFile(
    535                         AssetFileAddress.makeFromFile(dictionaryFile),
    536                         LocaleUtils.constructLocaleFromString(locale));
    537                 if (dictionaryInfo != null) {
    538                     addOrUpdateDictInfo(dictList, dictionaryInfo);
    539                 }
    540             }
    541         }
    542 
    543         // Retrieve files from assets
    544         final Resources resources = context.getResources();
    545         final AssetManager assets = resources.getAssets();
    546         for (final String localeString : assets.getLocales()) {
    547             final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
    548             final int resourceId =
    549                     DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale(
    550                             context.getResources(), locale);
    551             if (0 == resourceId) {
    552                 continue;
    553             }
    554             final AssetFileAddress fileAddress =
    555                     BinaryDictionaryGetter.loadFallbackResource(context, resourceId);
    556             final DictionaryInfo dictionaryInfo = createDictionaryInfoFromFileAddress(fileAddress,
    557                     locale);
    558             // Protect against cases of a less-specific dictionary being found, like an
    559             // en dictionary being used for an en_US locale. In this case, the en dictionary
    560             // should be used for en_US but discounted for listing purposes.
    561             // TODO: Remove dictionaryInfo == null when the static LMs have the headers.
    562             if (dictionaryInfo == null || !dictionaryInfo.mLocale.equals(locale)) {
    563                 continue;
    564             }
    565             addOrUpdateDictInfo(dictList, dictionaryInfo);
    566         }
    567 
    568         // Generate the dictionary information from  the enabled subtypes. This will not
    569         // overwrite the real records.
    570         RichInputMethodManager.init(context);
    571         List<InputMethodSubtype> enabledSubtypes = RichInputMethodManager
    572                 .getInstance().getMyEnabledInputMethodSubtypeList(true);
    573         for (InputMethodSubtype subtype : enabledSubtypes) {
    574             Locale locale = LocaleUtils.constructLocaleFromString(subtype.getLocale());
    575             DictionaryInfo dictionaryInfo = createDictionaryInfoFromLocale(locale);
    576             addOrUpdateDictInfo(dictList, dictionaryInfo);
    577         }
    578 
    579         return dictList;
    580     }
    581 
    582     @UsedForTesting
    583     public static boolean looksValidForDictionaryInsertion(final CharSequence text,
    584             final SpacingAndPunctuations spacingAndPunctuations) {
    585         if (TextUtils.isEmpty(text)) {
    586             return false;
    587         }
    588         final int length = text.length();
    589         if (length > DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH) {
    590             return false;
    591         }
    592         int i = 0;
    593         int digitCount = 0;
    594         while (i < length) {
    595             final int codePoint = Character.codePointAt(text, i);
    596             final int charCount = Character.charCount(codePoint);
    597             i += charCount;
    598             if (Character.isDigit(codePoint)) {
    599                 // Count digits: see below
    600                 digitCount += charCount;
    601                 continue;
    602             }
    603             if (!spacingAndPunctuations.isWordCodePoint(codePoint)) {
    604                 return false;
    605             }
    606         }
    607         // We reject strings entirely comprised of digits to avoid using PIN codes or credit
    608         // card numbers. It would come in handy for word prediction though; a good example is
    609         // when writing one's address where the street number is usually quite discriminative,
    610         // as well as the postal code.
    611         return digitCount < length;
    612     }
    613 }
    614