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