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.ContentResolver;
20 import android.content.Context;
21 import android.database.ContentObserver;
22 import android.database.Cursor;
23 import android.database.sqlite.SQLiteException;
24 import android.net.Uri;
25 import android.provider.UserDictionary.Words;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import com.android.inputmethod.annotations.ExternallyReferenced;
30 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
31 
32 import java.io.File;
33 import java.util.Arrays;
34 import java.util.Locale;
35 
36 import javax.annotation.Nullable;
37 
38 /**
39  * An expandable dictionary that stores the words in the user dictionary provider into a binary
40  * dictionary file to use it from native code.
41  */
42 public class UserBinaryDictionary extends ExpandableBinaryDictionary {
43     private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName();
44 
45     // The user dictionary provider uses an empty string to mean "all languages".
46     private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
47     private static final int HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY = 250;
48     private static final int LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY = 160;
49 
50     private static final String[] PROJECTION_QUERY = new String[] {Words.WORD, Words.FREQUENCY};
51 
52     private static final String NAME = "userunigram";
53 
54     private ContentObserver mObserver;
55     final private String mLocaleString;
56     final private boolean mAlsoUseMoreRestrictiveLocales;
57 
UserBinaryDictionary(final Context context, final Locale locale, final boolean alsoUseMoreRestrictiveLocales, final File dictFile, final String name)58     protected UserBinaryDictionary(final Context context, final Locale locale,
59                                    final boolean alsoUseMoreRestrictiveLocales,
60                                    final File dictFile, final String name) {
61         super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_USER, dictFile);
62         if (null == locale) throw new NullPointerException(); // Catch the error earlier
63         final String localeStr = locale.toString();
64         if (SubtypeLocaleUtils.NO_LANGUAGE.equals(localeStr)) {
65             // If we don't have a locale, insert into the "all locales" user dictionary.
66             mLocaleString = USER_DICTIONARY_ALL_LANGUAGES;
67         } else {
68             mLocaleString = localeStr;
69         }
70         mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
71         ContentResolver cres = context.getContentResolver();
72 
73         mObserver = new ContentObserver(null) {
74             @Override
75             public void onChange(final boolean self) {
76                 // This hook is deprecated as of API level 16 (Build.VERSION_CODES.JELLY_BEAN),
77                 // but should still be supported for cases where the IME is running on an older
78                 // version of the platform.
79                 onChange(self, null);
80             }
81             // The following hook is only available as of API level 16
82             // (Build.VERSION_CODES.JELLY_BEAN), and as such it will only work on JellyBean+
83             // devices. On older versions of the platform, the hook above will be called instead.
84             @Override
85             public void onChange(final boolean self, final Uri uri) {
86                 setNeedsToRecreate();
87             }
88         };
89         cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
90         reloadDictionaryIfRequired();
91     }
92 
93     // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
94     @ExternallyReferenced
getDictionary( final Context context, final Locale locale, final File dictFile, final String dictNamePrefix, @Nullable final String account)95     public static UserBinaryDictionary getDictionary(
96             final Context context, final Locale locale, final File dictFile,
97             final String dictNamePrefix, @Nullable final String account) {
98         return new UserBinaryDictionary(
99                 context, locale, false /* alsoUseMoreRestrictiveLocales */,
100                 dictFile, dictNamePrefix + NAME);
101     }
102 
103     @Override
close()104     public synchronized void close() {
105         if (mObserver != null) {
106             mContext.getContentResolver().unregisterContentObserver(mObserver);
107             mObserver = null;
108         }
109         super.close();
110     }
111 
112     @Override
loadInitialContentsLocked()113     public void loadInitialContentsLocked() {
114         // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
115         // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
116         // This is correct for locale processing.
117         // For this example, we'll look at the "en_US_POSIX" case.
118         final String[] localeElements =
119                 TextUtils.isEmpty(mLocaleString) ? new String[] {} : mLocaleString.split("_", 3);
120         final int length = localeElements.length;
121 
122         final StringBuilder request = new StringBuilder("(locale is NULL)");
123         String localeSoFar = "";
124         // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ;
125         // and request = "(locale is NULL)"
126         for (int i = 0; i < length; ++i) {
127             // i | localeSoFar    | localeElements
128             // 0 | ""             | ["en", "US", "POSIX"]
129             // 1 | "en_"          | ["en", "US", "POSIX"]
130             // 2 | "en_US_"       | ["en", "en_US", "POSIX"]
131             localeElements[i] = localeSoFar + localeElements[i];
132             localeSoFar = localeElements[i] + "_";
133             // i | request
134             // 0 | "(locale is NULL)"
135             // 1 | "(locale is NULL) or (locale=?)"
136             // 2 | "(locale is NULL) or (locale=?) or (locale=?)"
137             request.append(" or (locale=?)");
138         }
139         // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_"
140         // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)"
141 
142         final String[] requestArguments;
143         // If length == 3, we already have all the arguments we need (common prefix is meaningless
144         // inside variants
145         if (mAlsoUseMoreRestrictiveLocales && length < 3) {
146             request.append(" or (locale like ?)");
147             // The following creates an array with one more (null) position
148             final String[] localeElementsWithMoreRestrictiveLocalesIncluded =
149                     Arrays.copyOf(localeElements, length + 1);
150             localeElementsWithMoreRestrictiveLocalesIncluded[length] =
151                     localeElements[length - 1] + "_%";
152             requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded;
153             // If for example localeElements = ["en"]
154             // then requestArguments = ["en", "en_%"]
155             // and request = (locale is NULL) or (locale=?) or (locale like ?)
156             // If localeElements = ["en", "en_US"]
157             // then requestArguments = ["en", "en_US", "en_US_%"]
158         } else {
159             requestArguments = localeElements;
160         }
161         final String requestString = request.toString();
162         addWordsFromProjectionLocked(PROJECTION_QUERY, requestString, requestArguments);
163     }
164 
addWordsFromProjectionLocked(final String[] query, String request, final String[] requestArguments)165     private void addWordsFromProjectionLocked(final String[] query, String request,
166                                               final String[] requestArguments)
167             throws IllegalArgumentException {
168         Cursor cursor = null;
169         try {
170             cursor = mContext.getContentResolver().query(
171                     Words.CONTENT_URI, query, request, requestArguments, null);
172             addWordsLocked(cursor);
173         } catch (final SQLiteException e) {
174             Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
175         } finally {
176             try {
177                 if (null != cursor) cursor.close();
178             } catch (final SQLiteException e) {
179                 Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
180             }
181         }
182     }
183 
scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency)184     private static int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) {
185         // The default frequency for the user dictionary is 250 for historical reasons.
186         // Latin IME considers a good value for the default user dictionary frequency
187         // is about 160 considering the scale we use. So we are scaling down the values.
188         if (defaultFrequency > Integer.MAX_VALUE / LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY) {
189             return (defaultFrequency / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY)
190                     * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY;
191         }
192         return (defaultFrequency * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY)
193                 / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY;
194     }
195 
addWordsLocked(final Cursor cursor)196     private void addWordsLocked(final Cursor cursor) {
197         if (cursor == null) return;
198         if (cursor.moveToFirst()) {
199             final int indexWord = cursor.getColumnIndex(Words.WORD);
200             final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
201             while (!cursor.isAfterLast()) {
202                 final String word = cursor.getString(indexWord);
203                 final int frequency = cursor.getInt(indexFrequency);
204                 final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency);
205                 // Safeguard against adding really long words.
206                 if (word.length() <= MAX_WORD_LENGTH) {
207                     runGCIfRequiredLocked(true /* mindsBlockByGC */);
208                     addUnigramLocked(word, adjustedFrequency, false /* isNotAWord */,
209                             false /* isPossiblyOffensive */,
210                             BinaryDictionary.NOT_A_VALID_TIMESTAMP);
211                 }
212                 cursor.moveToNext();
213             }
214         }
215     }
216 }
217