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 
DictionaryInfo(@onnull String id, @Nonnull Locale locale, @Nullable String description, @Nullable String filename, long filesize, long modifiedTimeMillis, int version)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 
toContentValues()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
toString()108         public String toString() {
109             return "DictionaryInfo : Id = '" + mId
110                     + "' : Locale=" + mLocale
111                     + " : Version=" + mVersion;
112         }
113     }
114 
DictionaryInfoUtils()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      */
isFileNameCharacter(int codePoint)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
replaceFileNameDangerousCharacters(final String name)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      */
getWordListCacheDirectory(final Context context)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      */
getWordListStagingDirectory(final Context context)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      */
getWordListTempDirectory(final Context context)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
getWordListIdFromFileName(@onnull final String fname)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      */
getCachedDirectoryList(final Context context)202     public static File[] getCachedDirectoryList(final Context context) {
203         return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
204     }
205 
getStagingDirectoryList(final Context context)206     public static File[] getStagingDirectoryList(final Context context) {
207         return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles();
208     }
209 
210     @Nullable
getUnusedDictionaryList(final Context context)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