Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2012 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.ContentProviderClient;
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.database.ContentObserver;
     23 import android.database.Cursor;
     24 import android.database.sqlite.SQLiteException;
     25 import android.net.Uri;
     26 import android.os.Build;
     27 import android.provider.UserDictionary.Words;
     28 import android.text.TextUtils;
     29 import android.util.Log;
     30 
     31 import com.android.inputmethod.annotations.UsedForTesting;
     32 import com.android.inputmethod.compat.UserDictionaryCompatUtils;
     33 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
     34 
     35 import java.io.File;
     36 import java.util.Arrays;
     37 import java.util.Locale;
     38 
     39 /**
     40  * An expandable dictionary that stores the words in the user dictionary provider into a binary
     41  * dictionary file to use it from native code.
     42  */
     43 public class UserBinaryDictionary extends ExpandableBinaryDictionary {
     44     private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName();
     45 
     46     // The user dictionary provider uses an empty string to mean "all languages".
     47     private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
     48     private static final int HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY = 250;
     49     private static final int LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY = 160;
     50     // Shortcut frequency is 0~15, with 15 = whitelist. We don't want user dictionary entries
     51     // to auto-correct, so we set this to the highest frequency that won't, i.e. 14.
     52     private static final int USER_DICT_SHORTCUT_FREQUENCY = 14;
     53 
     54     private static final String[] PROJECTION_QUERY_WITH_SHORTCUT = new String[] {
     55         Words.WORD,
     56         Words.SHORTCUT,
     57         Words.FREQUENCY,
     58     };
     59     private static final String[] PROJECTION_QUERY_WITHOUT_SHORTCUT = new String[] {
     60         Words.WORD,
     61         Words.FREQUENCY,
     62     };
     63 
     64     private static final String NAME = "userunigram";
     65 
     66     private ContentObserver mObserver;
     67     final private String mLocale;
     68     final private boolean mAlsoUseMoreRestrictiveLocales;
     69 
     70     protected UserBinaryDictionary(final Context context, final Locale locale,
     71             final boolean alsoUseMoreRestrictiveLocales, final File dictFile, final String name) {
     72         super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_USER, dictFile);
     73         if (null == locale) throw new NullPointerException(); // Catch the error earlier
     74         final String localeStr = locale.toString();
     75         if (SubtypeLocaleUtils.NO_LANGUAGE.equals(localeStr)) {
     76             // If we don't have a locale, insert into the "all locales" user dictionary.
     77             mLocale = USER_DICTIONARY_ALL_LANGUAGES;
     78         } else {
     79             mLocale = localeStr;
     80         }
     81         mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
     82         ContentResolver cres = context.getContentResolver();
     83 
     84         mObserver = new ContentObserver(null) {
     85             @Override
     86             public void onChange(final boolean self) {
     87                 // This hook is deprecated as of API level 16 (Build.VERSION_CODES.JELLY_BEAN),
     88                 // but should still be supported for cases where the IME is running on an older
     89                 // version of the platform.
     90                 onChange(self, null);
     91             }
     92             // The following hook is only available as of API level 16
     93             // (Build.VERSION_CODES.JELLY_BEAN), and as such it will only work on JellyBean+
     94             // devices. On older versions of the platform, the hook above will be called instead.
     95             @Override
     96             public void onChange(final boolean self, final Uri uri) {
     97                 setNeedsToRecreate();
     98             }
     99         };
    100         cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
    101         reloadDictionaryIfRequired();
    102     }
    103 
    104     @UsedForTesting
    105     public static UserBinaryDictionary getDictionary(final Context context, final Locale locale,
    106             final File dictFile, final String dictNamePrefix) {
    107         return new UserBinaryDictionary(context, locale, false /* alsoUseMoreRestrictiveLocales */,
    108                 dictFile, dictNamePrefix + NAME);
    109     }
    110 
    111     @Override
    112     public synchronized void close() {
    113         if (mObserver != null) {
    114             mContext.getContentResolver().unregisterContentObserver(mObserver);
    115             mObserver = null;
    116         }
    117         super.close();
    118     }
    119 
    120     @Override
    121     public void loadInitialContentsLocked() {
    122         // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
    123         // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
    124         // This is correct for locale processing.
    125         // For this example, we'll look at the "en_US_POSIX" case.
    126         final String[] localeElements =
    127                 TextUtils.isEmpty(mLocale) ? new String[] {} : mLocale.split("_", 3);
    128         final int length = localeElements.length;
    129 
    130         final StringBuilder request = new StringBuilder("(locale is NULL)");
    131         String localeSoFar = "";
    132         // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ;
    133         // and request = "(locale is NULL)"
    134         for (int i = 0; i < length; ++i) {
    135             // i | localeSoFar    | localeElements
    136             // 0 | ""             | ["en", "US", "POSIX"]
    137             // 1 | "en_"          | ["en", "US", "POSIX"]
    138             // 2 | "en_US_"       | ["en", "en_US", "POSIX"]
    139             localeElements[i] = localeSoFar + localeElements[i];
    140             localeSoFar = localeElements[i] + "_";
    141             // i | request
    142             // 0 | "(locale is NULL)"
    143             // 1 | "(locale is NULL) or (locale=?)"
    144             // 2 | "(locale is NULL) or (locale=?) or (locale=?)"
    145             request.append(" or (locale=?)");
    146         }
    147         // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_"
    148         // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)"
    149 
    150         final String[] requestArguments;
    151         // If length == 3, we already have all the arguments we need (common prefix is meaningless
    152         // inside variants
    153         if (mAlsoUseMoreRestrictiveLocales && length < 3) {
    154             request.append(" or (locale like ?)");
    155             // The following creates an array with one more (null) position
    156             final String[] localeElementsWithMoreRestrictiveLocalesIncluded =
    157                     Arrays.copyOf(localeElements, length + 1);
    158             localeElementsWithMoreRestrictiveLocalesIncluded[length] =
    159                     localeElements[length - 1] + "_%";
    160             requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded;
    161             // If for example localeElements = ["en"]
    162             // then requestArguments = ["en", "en_%"]
    163             // and request = (locale is NULL) or (locale=?) or (locale like ?)
    164             // If localeElements = ["en", "en_US"]
    165             // then requestArguments = ["en", "en_US", "en_US_%"]
    166         } else {
    167             requestArguments = localeElements;
    168         }
    169         final String requestString = request.toString();
    170         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    171             try {
    172                 addWordsFromProjectionLocked(PROJECTION_QUERY_WITH_SHORTCUT, requestString,
    173                         requestArguments);
    174             } catch (IllegalArgumentException e) {
    175                 // This may happen on some non-compliant devices where the declared API is JB+ but
    176                 // the SHORTCUT column is not present for some reason.
    177                 addWordsFromProjectionLocked(PROJECTION_QUERY_WITHOUT_SHORTCUT, requestString,
    178                         requestArguments);
    179             }
    180         } else {
    181             addWordsFromProjectionLocked(PROJECTION_QUERY_WITHOUT_SHORTCUT, requestString,
    182                     requestArguments);
    183         }
    184     }
    185 
    186     private void addWordsFromProjectionLocked(final String[] query, String request,
    187             final String[] requestArguments) throws IllegalArgumentException {
    188         Cursor cursor = null;
    189         try {
    190             cursor = mContext.getContentResolver().query(
    191                     Words.CONTENT_URI, query, request, requestArguments, null);
    192             addWordsLocked(cursor);
    193         } catch (final SQLiteException e) {
    194             Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
    195         } finally {
    196             try {
    197                 if (null != cursor) cursor.close();
    198             } catch (final SQLiteException e) {
    199                 Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
    200             }
    201         }
    202     }
    203 
    204     public static boolean isEnabled(final Context context) {
    205         final ContentResolver cr = context.getContentResolver();
    206         final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI);
    207         if (client != null) {
    208             client.release();
    209             return true;
    210         } else {
    211             return false;
    212         }
    213     }
    214 
    215     /**
    216      * Adds a word to the user dictionary and makes it persistent.
    217      *
    218      * @param context the context
    219      * @param locale the locale
    220      * @param word the word to add. If the word is capitalized, then the dictionary will
    221      * recognize it as a capitalized word when searched.
    222      */
    223     public static void addWordToUserDictionary(final Context context, final Locale locale,
    224             final String word) {
    225         // Update the user dictionary provider
    226         UserDictionaryCompatUtils.addWord(context, word,
    227                 HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY, null, locale);
    228     }
    229 
    230     private int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) {
    231         // The default frequency for the user dictionary is 250 for historical reasons.
    232         // Latin IME considers a good value for the default user dictionary frequency
    233         // is about 160 considering the scale we use. So we are scaling down the values.
    234         if (defaultFrequency > Integer.MAX_VALUE / LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY) {
    235             return (defaultFrequency / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY)
    236                     * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY;
    237         } else {
    238             return (defaultFrequency * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY)
    239                     / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY;
    240         }
    241     }
    242 
    243     private void addWordsLocked(final Cursor cursor) {
    244         final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
    245         if (cursor == null) return;
    246         if (cursor.moveToFirst()) {
    247             final int indexWord = cursor.getColumnIndex(Words.WORD);
    248             final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(Words.SHORTCUT) : 0;
    249             final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
    250             while (!cursor.isAfterLast()) {
    251                 final String word = cursor.getString(indexWord);
    252                 final String shortcut = hasShortcutColumn ? cursor.getString(indexShortcut) : null;
    253                 final int frequency = cursor.getInt(indexFrequency);
    254                 final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency);
    255                 // Safeguard against adding really long words.
    256                 if (word.length() <= MAX_WORD_LENGTH) {
    257                     runGCIfRequiredLocked(true /* mindsBlockByGC */);
    258                     addUnigramLocked(word, adjustedFrequency, null /* shortcutTarget */,
    259                             0 /* shortcutFreq */, false /* isNotAWord */,
    260                             false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
    261                     if (null != shortcut && shortcut.length() <= MAX_WORD_LENGTH) {
    262                         runGCIfRequiredLocked(true /* mindsBlockByGC */);
    263                         addUnigramLocked(shortcut, adjustedFrequency, word,
    264                                 USER_DICT_SHORTCUT_FREQUENCY, true /* isNotAWord */,
    265                                 false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
    266                     }
    267                 }
    268                 cursor.moveToNext();
    269             }
    270         }
    271     }
    272 }
    273