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 17 package com.android.inputmethod.latin; 18 19 import static com.android.inputmethod.latin.PersonalDictionaryLookup.ANY_LOCALE; 20 21 import static org.mockito.Mockito.mock; 22 import static org.mockito.Mockito.times; 23 import static org.mockito.Mockito.verify; 24 import static org.mockito.Mockito.verifyNoMoreInteractions; 25 26 import android.annotation.SuppressLint; 27 import android.content.ContentResolver; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.provider.UserDictionary; 31 import android.test.AndroidTestCase; 32 import android.test.suitebuilder.annotation.SmallTest; 33 import android.util.Log; 34 35 import com.android.inputmethod.latin.PersonalDictionaryLookup.PersonalDictionaryListener; 36 import com.android.inputmethod.latin.utils.ExecutorUtils; 37 38 import java.util.HashSet; 39 import java.util.Locale; 40 import 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 49 public 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