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