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