1/*
2 * Copyright (C) 2015 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
17package com.android.inputmethod.latin;
18
19import static com.android.inputmethod.latin.PersonalDictionaryLookup.ANY_LOCALE;
20
21import static org.mockito.Mockito.mock;
22import static org.mockito.Mockito.times;
23import static org.mockito.Mockito.verify;
24import static org.mockito.Mockito.verifyNoMoreInteractions;
25
26import android.annotation.SuppressLint;
27import android.content.ContentResolver;
28import android.database.Cursor;
29import android.net.Uri;
30import android.provider.UserDictionary;
31import android.test.AndroidTestCase;
32import android.test.suitebuilder.annotation.SmallTest;
33import android.util.Log;
34
35import com.android.inputmethod.latin.PersonalDictionaryLookup.PersonalDictionaryListener;
36import com.android.inputmethod.latin.utils.ExecutorUtils;
37
38import java.util.HashSet;
39import java.util.Locale;
40import java.util.Set;
41
42/**
43 * Unit tests for {@link PersonalDictionaryLookup}.
44 *
45 * Note, this test doesn't mock out the ContentResolver, in order to make sure
46 * {@link PersonalDictionaryLookup} works in a real setting.
47 */
48@SmallTest
49public class PersonalDictionaryLookupTest extends AndroidTestCase {
50    private static final String TAG = PersonalDictionaryLookupTest.class.getSimpleName();
51
52    private ContentResolver mContentResolver;
53    private HashSet<Uri> mAddedBackup;
54
55    @Override
56    protected void setUp() throws Exception {
57        super.setUp();
58        mContentResolver = mContext.getContentResolver();
59        mAddedBackup = new HashSet<Uri>();
60    }
61
62    @Override
63    protected void tearDown() throws Exception {
64        // Remove all entries added during this test.
65        for (Uri row : mAddedBackup) {
66            mContentResolver.delete(row, null, null);
67        }
68        mAddedBackup.clear();
69
70        super.tearDown();
71    }
72
73    /**
74     * Adds the given word to the personal dictionary.
75     *
76     * @param word the word to add
77     * @param locale the locale of the word to add
78     * @param frequency the frequency of the word to add
79     * @return the Uri for the given word
80     */
81    @SuppressLint("NewApi")
82    private Uri addWord(final String word, final Locale locale, int frequency, String shortcut) {
83        // Add the given word for the given locale.
84        UserDictionary.Words.addWord(mContext, word, frequency, shortcut, locale);
85        // Obtain an Uri for the given word.
86        Cursor cursor = mContentResolver.query(UserDictionary.Words.CONTENT_URI, null,
87                UserDictionary.Words.WORD + "='" + word + "'", null, null);
88        assertTrue(cursor.moveToFirst());
89        Uri uri = Uri.withAppendedPath(UserDictionary.Words.CONTENT_URI,
90                cursor.getString(cursor.getColumnIndex(UserDictionary.Words._ID)));
91        // Add the row to the backup for later clearing.
92        mAddedBackup.add(uri);
93        return uri;
94    }
95
96    /**
97     * Deletes the entry for the given word from UserDictionary.
98     *
99     * @param uri the Uri for the word as returned by addWord
100     */
101    private void deleteWord(Uri uri) {
102        // Remove the word from the backup so that it's not cleared again later.
103        mAddedBackup.remove(uri);
104        // Remove the word from the personal dictionary.
105        mContentResolver.delete(uri, null, null);
106    }
107
108    private PersonalDictionaryLookup setUpWord(final Locale locale) {
109        // Insert "foo" in the personal dictionary for the given locale.
110        addWord("foo", locale, 17, null);
111
112        // Create the PersonalDictionaryLookup and wait until it's loaded.
113        PersonalDictionaryLookup lookup =
114                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
115        lookup.open();
116        return lookup;
117    }
118
119    private PersonalDictionaryLookup setUpShortcut(final Locale locale) {
120        // Insert "shortcut" => "Expansion" in the personal dictionary for the given locale.
121        addWord("Expansion", locale, 17, "shortcut");
122
123        // Create the PersonalDictionaryLookup and wait until it's loaded.
124        PersonalDictionaryLookup lookup =
125                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
126        lookup.open();
127        return lookup;
128    }
129
130    private void verifyWordExists(final Set<String> set, final String word) {
131        assertTrue(set.contains(word));
132    }
133
134    private void verifyWordDoesNotExist(final Set<String> set, final String word) {
135        assertFalse(set.contains(word));
136    }
137
138    public void testShortcutKeyMatching() {
139        Log.d(TAG, "testShortcutKeyMatching");
140        PersonalDictionaryLookup lookup = setUpShortcut(Locale.US);
141
142        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
143        assertNull(lookup.expandShortcut("Shortcut", Locale.US));
144        assertNull(lookup.expandShortcut("SHORTCUT", Locale.US));
145        assertNull(lookup.expandShortcut("shortcu", Locale.US));
146        assertNull(lookup.expandShortcut("shortcutt", Locale.US));
147
148        lookup.close();
149    }
150
151    public void testShortcutMatchesInputCountry() {
152        Log.d(TAG, "testShortcutMatchesInputCountry");
153        PersonalDictionaryLookup lookup = setUpShortcut(Locale.US);
154
155        verifyWordExists(lookup.getShortcutsForLocale(Locale.US), "shortcut");
156        assertTrue(lookup.getShortcutsForLocale(Locale.UK).isEmpty());
157        assertTrue(lookup.getShortcutsForLocale(Locale.ENGLISH).isEmpty());
158        assertTrue(lookup.getShortcutsForLocale(Locale.FRENCH).isEmpty());
159        assertTrue(lookup.getShortcutsForLocale(ANY_LOCALE).isEmpty());
160
161        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
162        assertNull(lookup.expandShortcut("shortcut", Locale.UK));
163        assertNull(lookup.expandShortcut("shortcut", Locale.ENGLISH));
164        assertNull(lookup.expandShortcut("shortcut", Locale.FRENCH));
165        assertNull(lookup.expandShortcut("shortcut", ANY_LOCALE));
166
167        lookup.close();
168    }
169
170    public void testShortcutMatchesInputLanguage() {
171        Log.d(TAG, "testShortcutMatchesInputLanguage");
172        PersonalDictionaryLookup lookup = setUpShortcut(Locale.ENGLISH);
173
174        verifyWordExists(lookup.getShortcutsForLocale(Locale.US), "shortcut");
175        verifyWordExists(lookup.getShortcutsForLocale(Locale.UK), "shortcut");
176        verifyWordExists(lookup.getShortcutsForLocale(Locale.ENGLISH), "shortcut");
177        assertTrue(lookup.getShortcutsForLocale(Locale.FRENCH).isEmpty());
178        assertTrue(lookup.getShortcutsForLocale(ANY_LOCALE).isEmpty());
179
180        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
181        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.UK));
182        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.ENGLISH));
183        assertNull(lookup.expandShortcut("shortcut", Locale.FRENCH));
184        assertNull(lookup.expandShortcut("shortcut", ANY_LOCALE));
185
186        lookup.close();
187    }
188
189    public void testShortcutMatchesAnyLocale() {
190        PersonalDictionaryLookup lookup = setUpShortcut(PersonalDictionaryLookup.ANY_LOCALE);
191
192        verifyWordExists(lookup.getShortcutsForLocale(Locale.US), "shortcut");
193        verifyWordExists(lookup.getShortcutsForLocale(Locale.UK), "shortcut");
194        verifyWordExists(lookup.getShortcutsForLocale(Locale.ENGLISH), "shortcut");
195        verifyWordExists(lookup.getShortcutsForLocale(Locale.FRENCH), "shortcut");
196        verifyWordExists(lookup.getShortcutsForLocale(ANY_LOCALE), "shortcut");
197
198        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.US));
199        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.UK));
200        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.ENGLISH));
201        assertEquals("Expansion", lookup.expandShortcut("shortcut", Locale.FRENCH));
202        assertEquals("Expansion", lookup.expandShortcut("shortcut", ANY_LOCALE));
203
204        lookup.close();
205    }
206
207    public void testExactLocaleMatch() {
208        Log.d(TAG, "testExactLocaleMatch");
209        PersonalDictionaryLookup lookup = setUpWord(Locale.US);
210
211        verifyWordExists(lookup.getWordsForLocale(Locale.US), "foo");
212        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.UK), "foo");
213        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.ENGLISH), "foo");
214        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.FRENCH), "foo");
215        verifyWordDoesNotExist(lookup.getWordsForLocale(ANY_LOCALE), "foo");
216
217        // Any capitalization variation should match.
218        assertTrue(lookup.isValidWord("foo", Locale.US));
219        assertTrue(lookup.isValidWord("Foo", Locale.US));
220        assertTrue(lookup.isValidWord("FOO", Locale.US));
221        // But similar looking words don't match.
222        assertFalse(lookup.isValidWord("fo", Locale.US));
223        assertFalse(lookup.isValidWord("fop", Locale.US));
224        assertFalse(lookup.isValidWord("fooo", Locale.US));
225        // Other locales, including more general locales won't match.
226        assertFalse(lookup.isValidWord("foo", Locale.ENGLISH));
227        assertFalse(lookup.isValidWord("foo", Locale.UK));
228        assertFalse(lookup.isValidWord("foo", Locale.FRENCH));
229        assertFalse(lookup.isValidWord("foo", ANY_LOCALE));
230
231        lookup.close();
232    }
233
234    public void testSubLocaleMatch() {
235        Log.d(TAG, "testSubLocaleMatch");
236        PersonalDictionaryLookup lookup = setUpWord(Locale.ENGLISH);
237
238        verifyWordExists(lookup.getWordsForLocale(Locale.US), "foo");
239        verifyWordExists(lookup.getWordsForLocale(Locale.UK), "foo");
240        verifyWordExists(lookup.getWordsForLocale(Locale.ENGLISH), "foo");
241        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.FRENCH), "foo");
242        verifyWordDoesNotExist(lookup.getWordsForLocale(ANY_LOCALE), "foo");
243
244        // Any capitalization variation should match for both en and en_US.
245        assertTrue(lookup.isValidWord("foo", Locale.ENGLISH));
246        assertTrue(lookup.isValidWord("foo", Locale.US));
247        assertTrue(lookup.isValidWord("Foo", Locale.US));
248        assertTrue(lookup.isValidWord("FOO", Locale.US));
249        // But similar looking words don't match.
250        assertFalse(lookup.isValidWord("fo", Locale.US));
251        assertFalse(lookup.isValidWord("fop", Locale.US));
252        assertFalse(lookup.isValidWord("fooo", Locale.US));
253
254        lookup.close();
255    }
256
257    public void testAllLocalesMatch() {
258        Log.d(TAG, "testAllLocalesMatch");
259        PersonalDictionaryLookup lookup = setUpWord(null);
260
261        verifyWordExists(lookup.getWordsForLocale(Locale.US), "foo");
262        verifyWordExists(lookup.getWordsForLocale(Locale.UK), "foo");
263        verifyWordExists(lookup.getWordsForLocale(Locale.ENGLISH), "foo");
264        verifyWordExists(lookup.getWordsForLocale(Locale.FRENCH), "foo");
265        verifyWordExists(lookup.getWordsForLocale(ANY_LOCALE), "foo");
266
267        // Any capitalization variation should match for fr, en and en_US.
268        assertTrue(lookup.isValidWord("foo", ANY_LOCALE));
269        assertTrue(lookup.isValidWord("foo", Locale.FRENCH));
270        assertTrue(lookup.isValidWord("foo", Locale.ENGLISH));
271        assertTrue(lookup.isValidWord("foo", Locale.US));
272        assertTrue(lookup.isValidWord("Foo", Locale.US));
273        assertTrue(lookup.isValidWord("FOO", Locale.US));
274        // But similar looking words don't match.
275        assertFalse(lookup.isValidWord("fo", Locale.US));
276        assertFalse(lookup.isValidWord("fop", Locale.US));
277        assertFalse(lookup.isValidWord("fooo", Locale.US));
278
279        lookup.close();
280    }
281
282    public void testMultipleLocalesMatch() {
283        Log.d(TAG, "testMultipleLocalesMatch");
284
285        // Insert "Foo" as capitalized in the personal dictionary under the en_US and en_CA and fr
286        // locales.
287        addWord("Foo", Locale.US, 17, null);
288        addWord("foO", Locale.CANADA, 17, null);
289        addWord("fOo", Locale.FRENCH, 17, null);
290
291        // Create the PersonalDictionaryLookup and wait until it's loaded.
292        PersonalDictionaryLookup lookup = new PersonalDictionaryLookup(mContext,
293                ExecutorUtils.SPELLING);
294        lookup.open();
295
296        // Both en_CA and en_US match.
297        assertTrue(lookup.isValidWord("foo", Locale.CANADA));
298        assertTrue(lookup.isValidWord("foo", Locale.US));
299        assertTrue(lookup.isValidWord("foo", Locale.FRENCH));
300        // Other locales, including more general locales won't match.
301        assertFalse(lookup.isValidWord("foo", Locale.ENGLISH));
302        assertFalse(lookup.isValidWord("foo", Locale.UK));
303        assertFalse(lookup.isValidWord("foo", ANY_LOCALE));
304
305        lookup.close();
306    }
307
308
309    public void testCaseMatchingForWordsAndShortcuts() {
310        Log.d(TAG, "testCaseMatchingForWordsAndShortcuts");
311        addWord("Foo", Locale.US, 17, "f");
312        addWord("bokabu", Locale.US, 17, "Bu");
313
314        // Create the PersonalDictionaryLookup and wait until it's loaded.
315        PersonalDictionaryLookup lookup = new PersonalDictionaryLookup(mContext,
316                ExecutorUtils.SPELLING);
317        lookup.open();
318
319        // Valid, inspite of capitalization in US but not in other
320        // locales.
321        assertTrue(lookup.isValidWord("Foo", Locale.US));
322        assertTrue(lookup.isValidWord("foo", Locale.US));
323        assertFalse(lookup.isValidWord("Foo", Locale.UK));
324        assertFalse(lookup.isValidWord("foo", Locale.UK));
325
326        // Valid in all forms in US.
327        assertTrue(lookup.isValidWord("bokabu", Locale.US));
328        assertTrue(lookup.isValidWord("BOKABU", Locale.US));
329        assertTrue(lookup.isValidWord("BokaBU", Locale.US));
330
331        // Correct capitalization; sensitive to shortcut casing & locale.
332        assertEquals("Foo", lookup.expandShortcut("f", Locale.US));
333        assertNull(lookup.expandShortcut("f", Locale.UK));
334
335        // Correct capitalization; sensitive to shortcut casing & locale.
336        assertEquals("bokabu", lookup.expandShortcut("Bu", Locale.US));
337        assertNull(lookup.expandShortcut("Bu", Locale.UK));
338        assertNull(lookup.expandShortcut("bu", Locale.US));
339
340        // Verify that raw strings are retained for #getWordsForLocale.
341        verifyWordExists(lookup.getWordsForLocale(Locale.US), "Foo");
342        verifyWordDoesNotExist(lookup.getWordsForLocale(Locale.US), "foo");
343    }
344
345    public void testManageListeners() {
346        Log.d(TAG, "testManageListeners");
347
348        PersonalDictionaryLookup lookup =
349                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
350
351        PersonalDictionaryListener listener = mock(PersonalDictionaryListener.class);
352        // Add the same listener a bunch of times. It doesn't make a difference.
353        lookup.addListener(listener);
354        lookup.addListener(listener);
355        lookup.addListener(listener);
356        lookup.notifyListeners();
357
358        verify(listener, times(1)).onUpdate();
359
360        // Remove the same listener a bunch of times. It doesn't make a difference.
361        lookup.removeListener(listener);
362        lookup.removeListener(listener);
363        lookup.removeListener(listener);
364        lookup.notifyListeners();
365
366        verifyNoMoreInteractions(listener);
367    }
368
369    public void testReload() {
370        Log.d(TAG, "testReload");
371
372        // Insert "foo".
373        Uri uri = addWord("foo", Locale.US, 17, null);
374
375        // Create the PersonalDictionaryLookup and wait until it's loaded.
376        PersonalDictionaryLookup lookup =
377                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
378        lookup.open();
379
380        // "foo" should match.
381        assertTrue(lookup.isValidWord("foo", Locale.US));
382
383        // "bar" shouldn't match.
384        assertFalse(lookup.isValidWord("bar", Locale.US));
385
386        // Now delete "foo" and add "bar".
387        deleteWord(uri);
388        addWord("bar", Locale.US, 18, null);
389
390        // Wait a little bit before expecting a change. The time we wait should be greater than
391        // PersonalDictionaryLookup.RELOAD_DELAY_MS.
392        try {
393            Thread.sleep(PersonalDictionaryLookup.RELOAD_DELAY_MS + 1000);
394        } catch (InterruptedException e) {
395        }
396
397        // Perform lookups again. Reload should have occured.
398        //
399        // "foo" should not match.
400        assertFalse(lookup.isValidWord("foo", Locale.US));
401
402        // "bar" should match.
403        assertTrue(lookup.isValidWord("bar", Locale.US));
404
405        lookup.close();
406    }
407
408    public void testDictionaryStats() {
409        Log.d(TAG, "testDictionaryStats");
410
411        // Insert "foo" and "bar". Only "foo" has a shortcut.
412        Uri uri = addWord("foo", Locale.GERMANY, 17, "f");
413        addWord("bar", Locale.GERMANY, 17, null);
414
415        // Create the PersonalDictionaryLookup and wait until it's loaded.
416        PersonalDictionaryLookup lookup =
417                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
418        lookup.open();
419
420        // "foo" should match.
421        assertTrue(lookup.isValidWord("foo", Locale.GERMANY));
422
423        // "bar" should match.
424        assertTrue(lookup.isValidWord("bar", Locale.GERMANY));
425
426        // "foo" should have a shortcut.
427        assertEquals("foo", lookup.expandShortcut("f", Locale.GERMANY));
428
429        // Now delete "foo".
430        deleteWord(uri);
431
432        // Wait a little bit before expecting a change. The time we wait should be greater than
433        // PersonalDictionaryLookup.RELOAD_DELAY_MS.
434        try {
435            Thread.sleep(PersonalDictionaryLookup.RELOAD_DELAY_MS + 1000);
436        } catch (InterruptedException e) {
437        }
438
439        // Perform lookups again. Reload should have occured.
440        //
441        // "foo" should not match.
442        assertFalse(lookup.isValidWord("foo", Locale.GERMANY));
443
444        // "foo" should not have a shortcut.
445        assertNull(lookup.expandShortcut("f", Locale.GERMANY));
446
447        // "bar" should still match.
448        assertTrue(lookup.isValidWord("bar", Locale.GERMANY));
449
450        lookup.close();
451    }
452
453    public void testClose() {
454        Log.d(TAG, "testClose");
455
456        // Insert "foo".
457        Uri uri = addWord("foo", Locale.US, 17, null);
458
459        // Create the PersonalDictionaryLookup and wait until it's loaded.
460        PersonalDictionaryLookup lookup =
461                new PersonalDictionaryLookup(mContext, ExecutorUtils.SPELLING);
462        lookup.open();
463
464        // "foo" should match.
465        assertTrue(lookup.isValidWord("foo", Locale.US));
466
467        // "bar" shouldn't match.
468        assertFalse(lookup.isValidWord("bar", Locale.US));
469
470        // Now close (prevents further reloads).
471        lookup.close();
472
473        // Now delete "foo" and add "bar".
474        deleteWord(uri);
475        addWord("bar", Locale.US, 18, null);
476
477        // Wait a little bit before expecting a change. The time we wait should be greater than
478        // PersonalDictionaryLookup.RELOAD_DELAY_MS.
479        try {
480            Thread.sleep(PersonalDictionaryLookup.RELOAD_DELAY_MS + 1000);
481        } catch (InterruptedException e) {
482        }
483
484        // Perform lookups again. Reload should not have occurred.
485        //
486        // "foo" should stil match.
487        assertTrue(lookup.isValidWord("foo", Locale.US));
488
489        // "bar" should still not match.
490        assertFalse(lookup.isValidWord("bar", Locale.US));
491    }
492}
493