1 /* 2 * Copyright (C) 2017 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.settings.inputmethod; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.database.MatrixCursor; 22 import android.provider.UserDictionary; 23 import android.util.ArraySet; 24 25 import androidx.annotation.VisibleForTesting; 26 import androidx.loader.content.CursorLoader; 27 28 import java.util.Locale; 29 import java.util.Objects; 30 import java.util.Set; 31 32 public class UserDictionaryCursorLoader extends CursorLoader { 33 34 @VisibleForTesting 35 static final String[] QUERY_PROJECTION = { 36 UserDictionary.Words._ID, 37 UserDictionary.Words.WORD, 38 UserDictionary.Words.SHORTCUT 39 }; 40 41 // The index of the shortcut in the above array. 42 static final int INDEX_SHORTCUT = 2; 43 44 // Either the locale is empty (means the word is applicable to all locales) 45 // or the word equals our current locale 46 private static final String QUERY_SELECTION = 47 UserDictionary.Words.LOCALE + "=?"; 48 private static final String QUERY_SELECTION_ALL_LOCALES = 49 UserDictionary.Words.LOCALE + " is null"; 50 51 52 // Locale can be any of: 53 // - The string representation of a locale, as returned by Locale#toString() 54 // - The empty string. This means we want a cursor returning words valid for all locales. 55 // - null. This means we want a cursor for the current locale, whatever this is. 56 // Note that this contrasts with the data inside the database, where NULL means "all 57 // locales" and there should never be an empty string. The confusion is called by the 58 // historical use of null for "all locales". 59 // TODO: it should be easy to make this more readable by making the special values 60 // human-readable, like "all_locales" and "current_locales" strings, provided they 61 // can be guaranteed not to match locales that may exist. 62 private final String mLocale; 63 UserDictionaryCursorLoader(Context context, String locale)64 public UserDictionaryCursorLoader(Context context, String locale) { 65 super(context); 66 mLocale = locale; 67 } 68 69 @Override loadInBackground()70 public Cursor loadInBackground() { 71 final MatrixCursor result = new MatrixCursor(QUERY_PROJECTION); 72 final Cursor candidate; 73 if ("".equals(mLocale)) { 74 // Case-insensitive sort 75 candidate = getContext().getContentResolver().query( 76 UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, 77 QUERY_SELECTION_ALL_LOCALES, null, 78 "UPPER(" + UserDictionary.Words.WORD + ")"); 79 } else { 80 final String queryLocale = null != mLocale ? mLocale : Locale.getDefault().toString(); 81 candidate = getContext().getContentResolver().query(UserDictionary.Words.CONTENT_URI, 82 QUERY_PROJECTION, QUERY_SELECTION, 83 new String[]{queryLocale}, "UPPER(" + UserDictionary.Words.WORD + ")"); 84 } 85 final Set<Integer> hashSet = new ArraySet<>(); 86 for (candidate.moveToFirst(); !candidate.isAfterLast(); candidate.moveToNext()) { 87 final int id = candidate.getInt(0); 88 final String word = candidate.getString(1); 89 final String shortcut = candidate.getString(2); 90 final int hash = Objects.hash(word, shortcut); 91 if (hashSet.contains(hash)) { 92 continue; 93 } 94 hashSet.add(hash); 95 result.addRow(new Object[]{id, word, shortcut}); 96 } 97 return result; 98 } 99 } 100