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