• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.providers.userdictionary;
18 
19 import java.util.List;
20 
21 import android.app.backup.BackupManager;
22 import android.content.ContentProvider;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.UriMatcher;
27 import android.database.Cursor;
28 import android.database.MatrixCursor;
29 import android.database.SQLException;
30 import android.database.sqlite.SQLiteDatabase;
31 import android.database.sqlite.SQLiteOpenHelper;
32 import android.database.sqlite.SQLiteQueryBuilder;
33 import android.net.Uri;
34 import android.os.Binder;
35 import android.os.Process;
36 import android.provider.UserDictionary;
37 import android.provider.UserDictionary.Words;
38 import android.text.TextUtils;
39 import android.util.ArrayMap;
40 import android.util.Log;
41 import android.view.inputmethod.InputMethodInfo;
42 import android.view.inputmethod.InputMethodManager;
43 import android.view.textservice.SpellCheckerInfo;
44 import android.view.textservice.TextServicesManager;
45 
46 /**
47  * Provides access to a database of user defined words. Each item has a word and a frequency.
48  */
49 public class UserDictionaryProvider extends ContentProvider {
50 
51     /**
52      * DB versions are as follow:
53      *
54      * Version 1:
55      *   Up to IceCreamSandwich 4.0.3 - API version 15
56      *   Contient ID (INTEGER PRIMARY KEY), WORD (TEXT), FREQUENCY (INTEGER),
57      *   LOCALE (TEXT), APP_ID (INTEGER).
58      *
59      * Version 2:
60      *   From IceCreamSandwich, 4.1 - API version 16
61      *   Adds SHORTCUT (TEXT).
62      */
63 
64     private static final String AUTHORITY = UserDictionary.AUTHORITY;
65 
66     private static final String TAG = "UserDictionaryProvider";
67 
68     private static final String DATABASE_NAME = "user_dict.db";
69     private static final int DATABASE_VERSION = 2;
70 
71     private static final String USERDICT_TABLE_NAME = "words";
72 
73     private static ArrayMap<String, String> sDictProjectionMap;
74 
75     private static final UriMatcher sUriMatcher;
76 
77     private static final int WORDS = 1;
78 
79     private static final int WORD_ID = 2;
80 
81     static {
82         sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, "words", WORDS)83         sUriMatcher.addURI(AUTHORITY, "words", WORDS);
sUriMatcher.addURI(AUTHORITY, "words/#", WORD_ID)84         sUriMatcher.addURI(AUTHORITY, "words/#", WORD_ID);
85 
86         sDictProjectionMap = new ArrayMap<>();
sDictProjectionMap.put(Words._ID, Words._ID)87         sDictProjectionMap.put(Words._ID, Words._ID);
sDictProjectionMap.put(Words.WORD, Words.WORD)88         sDictProjectionMap.put(Words.WORD, Words.WORD);
sDictProjectionMap.put(Words.FREQUENCY, Words.FREQUENCY)89         sDictProjectionMap.put(Words.FREQUENCY, Words.FREQUENCY);
sDictProjectionMap.put(Words.LOCALE, Words.LOCALE)90         sDictProjectionMap.put(Words.LOCALE, Words.LOCALE);
sDictProjectionMap.put(Words.APP_ID, Words.APP_ID)91         sDictProjectionMap.put(Words.APP_ID, Words.APP_ID);
sDictProjectionMap.put(Words.SHORTCUT, Words.SHORTCUT)92         sDictProjectionMap.put(Words.SHORTCUT, Words.SHORTCUT);
93     }
94 
95     private BackupManager mBackupManager;
96     private InputMethodManager mImeManager;
97     private TextServicesManager mTextServiceManager;
98 
99     /**
100      * This class helps open, create, and upgrade the database file.
101      */
102     private static class DatabaseHelper extends SQLiteOpenHelper {
103 
DatabaseHelper(Context context)104         DatabaseHelper(Context context) {
105             super(context, DATABASE_NAME, null, DATABASE_VERSION);
106         }
107 
108         @Override
onCreate(SQLiteDatabase db)109         public void onCreate(SQLiteDatabase db) {
110             db.execSQL("CREATE TABLE " + USERDICT_TABLE_NAME + " ("
111                     + Words._ID + " INTEGER PRIMARY KEY,"
112                     + Words.WORD + " TEXT,"
113                     + Words.FREQUENCY + " INTEGER,"
114                     + Words.LOCALE + " TEXT,"
115                     + Words.APP_ID + " INTEGER,"
116                     + Words.SHORTCUT + " TEXT"
117                     + ");");
118         }
119 
120         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)121         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
122             if (oldVersion == 1 && newVersion == 2) {
123                 Log.i(TAG, "Upgrading database from version " + oldVersion
124                         + " to version 2: adding " + Words.SHORTCUT + " column");
125                 db.execSQL("ALTER TABLE " + USERDICT_TABLE_NAME
126                         + " ADD " + Words.SHORTCUT + " TEXT;");
127             } else {
128                 Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
129                         + newVersion + ", which will destroy all old data");
130                 db.execSQL("DROP TABLE IF EXISTS " + USERDICT_TABLE_NAME);
131                 onCreate(db);
132             }
133         }
134     }
135 
136     private DatabaseHelper mOpenHelper;
137 
138     @Override
onCreate()139     public boolean onCreate() {
140         mOpenHelper = new DatabaseHelper(getContext());
141         mBackupManager = new BackupManager(getContext());
142         mImeManager = getContext().getSystemService(InputMethodManager.class);
143         mTextServiceManager = getContext().getSystemService(TextServicesManager.class);
144         return true;
145     }
146 
147     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)148     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
149             String sortOrder) {
150         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
151 
152         switch (sUriMatcher.match(uri)) {
153             case WORDS:
154                 qb.setTables(USERDICT_TABLE_NAME);
155                 qb.setProjectionMap(sDictProjectionMap);
156                 break;
157 
158             case WORD_ID:
159                 qb.setTables(USERDICT_TABLE_NAME);
160                 qb.setProjectionMap(sDictProjectionMap);
161                 qb.appendWhere("_id" + "=" + uri.getPathSegments().get(1));
162                 break;
163 
164             default:
165                 throw new IllegalArgumentException("Unknown URI " + uri);
166         }
167 
168         // Only the enabled IMEs and spell checkers can access this provider.
169         if (!canCallerAccessUserDictionary()) {
170             return getEmptyCursorOrThrow(projection);
171         }
172 
173         // If no sort order is specified use the default
174         String orderBy;
175         if (TextUtils.isEmpty(sortOrder)) {
176             orderBy = Words.DEFAULT_SORT_ORDER;
177         } else {
178             orderBy = sortOrder;
179         }
180 
181         // Get the database and run the query
182         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
183         Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
184 
185         // Tell the cursor what uri to watch, so it knows when its source data changes
186         c.setNotificationUri(getContext().getContentResolver(), uri);
187         return c;
188     }
189 
190     @Override
getType(Uri uri)191     public String getType(Uri uri) {
192         switch (sUriMatcher.match(uri)) {
193             case WORDS:
194                 return Words.CONTENT_TYPE;
195 
196             case WORD_ID:
197                 return Words.CONTENT_ITEM_TYPE;
198 
199             default:
200                 throw new IllegalArgumentException("Unknown URI " + uri);
201         }
202     }
203 
204     @Override
insert(Uri uri, ContentValues initialValues)205     public Uri insert(Uri uri, ContentValues initialValues) {
206         // Validate the requested uri
207         if (sUriMatcher.match(uri) != WORDS) {
208             throw new IllegalArgumentException("Unknown URI " + uri);
209         }
210 
211         // Only the enabled IMEs and spell checkers can access this provider.
212         if (!canCallerAccessUserDictionary()) {
213             return null;
214         }
215 
216         ContentValues values;
217         if (initialValues != null) {
218             values = new ContentValues(initialValues);
219         } else {
220             values = new ContentValues();
221         }
222 
223         if (!values.containsKey(Words.WORD)) {
224             throw new SQLException("Word must be specified");
225         }
226 
227         if (!values.containsKey(Words.FREQUENCY)) {
228             values.put(Words.FREQUENCY, "1");
229         }
230 
231         if (!values.containsKey(Words.LOCALE)) {
232             values.put(Words.LOCALE, (String) null);
233         }
234 
235         if (!values.containsKey(Words.SHORTCUT)) {
236             values.put(Words.SHORTCUT, (String) null);
237         }
238 
239         values.put(Words.APP_ID, 0);
240 
241         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
242         long rowId = db.insert(USERDICT_TABLE_NAME, Words.WORD, values);
243         if (rowId > 0) {
244             Uri wordUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, rowId);
245             getContext().getContentResolver().notifyChange(wordUri, null);
246             mBackupManager.dataChanged();
247             return wordUri;
248         }
249 
250         throw new SQLException("Failed to insert row into " + uri);
251     }
252 
253     @Override
delete(Uri uri, String where, String[] whereArgs)254     public int delete(Uri uri, String where, String[] whereArgs) {
255         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
256         int count;
257         switch (sUriMatcher.match(uri)) {
258             case WORDS:
259                 count = db.delete(USERDICT_TABLE_NAME, where, whereArgs);
260                 break;
261 
262             case WORD_ID:
263                 String wordId = uri.getPathSegments().get(1);
264                 count = db.delete(USERDICT_TABLE_NAME, Words._ID + "=" + wordId
265                         + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
266                 break;
267 
268             default:
269                 throw new IllegalArgumentException("Unknown URI " + uri);
270         }
271 
272         // Only the enabled IMEs and spell checkers can access this provider.
273         if (!canCallerAccessUserDictionary()) {
274             return 0;
275         }
276 
277         getContext().getContentResolver().notifyChange(uri, null);
278         mBackupManager.dataChanged();
279         return count;
280     }
281 
282     @Override
update(Uri uri, ContentValues values, String where, String[] whereArgs)283     public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
284         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
285         int count;
286         switch (sUriMatcher.match(uri)) {
287             case WORDS:
288                 count = db.update(USERDICT_TABLE_NAME, values, where, whereArgs);
289                 break;
290 
291             case WORD_ID:
292                 String wordId = uri.getPathSegments().get(1);
293                 count = db.update(USERDICT_TABLE_NAME, values, Words._ID + "=" + wordId
294                         + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
295                 break;
296 
297             default:
298                 throw new IllegalArgumentException("Unknown URI " + uri);
299         }
300 
301         // Only the enabled IMEs and spell checkers can access this provider.
302         if (!canCallerAccessUserDictionary()) {
303             return 0;
304         }
305 
306         getContext().getContentResolver().notifyChange(uri, null);
307         mBackupManager.dataChanged();
308         return count;
309     }
310 
canCallerAccessUserDictionary()311     private boolean canCallerAccessUserDictionary() {
312         final int callingUid = Binder.getCallingUid();
313 
314         if (callingUid == Process.SYSTEM_UID
315                 || callingUid == Process.ROOT_UID
316                 || callingUid == Process.myUid()) {
317             return true;
318         }
319 
320         String callingPackage = getCallingPackage();
321 
322         List<InputMethodInfo> imeInfos = mImeManager.getEnabledInputMethodList();
323         if (imeInfos != null) {
324             final int imeInfoCount = imeInfos.size();
325             for (int i = 0; i < imeInfoCount; i++) {
326                 InputMethodInfo imeInfo = imeInfos.get(i);
327                 if (imeInfo.getServiceInfo().applicationInfo.uid == callingUid
328                         && imeInfo.getPackageName().equals(callingPackage)) {
329                     return true;
330                 }
331             }
332         }
333 
334         SpellCheckerInfo[] scInfos = mTextServiceManager.getEnabledSpellCheckers();
335         if (scInfos != null) {
336             for (SpellCheckerInfo scInfo : scInfos) {
337                 if (scInfo.getServiceInfo().applicationInfo.uid == callingUid
338                         && scInfo.getPackageName().equals(callingPackage)) {
339                     return true;
340                 }
341             }
342         }
343 
344         return false;
345     }
346 
getEmptyCursorOrThrow(String[] projection)347     private static Cursor getEmptyCursorOrThrow(String[] projection) {
348         if (projection != null) {
349             for (String column : projection) {
350                 if (sDictProjectionMap.get(column) == null) {
351                     throw new IllegalArgumentException("Unknown column: " + column);
352                 }
353             }
354         } else {
355             final int columnCount = sDictProjectionMap.size();
356             projection = new String[columnCount];
357             for (int i = 0; i < columnCount; i++) {
358                 projection[i] = sDictProjectionMap.keyAt(i);
359             }
360         }
361 
362         return new MatrixCursor(projection, 0);
363     }
364 }
365