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