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); 84 sUriMatcher.addURI(AUTHORITY, "words", WORDS); 85 sUriMatcher.addURI(AUTHORITY, "words/#", WORD_ID); 86 87 sDictProjectionMap = new ArrayMap<>(); 88 sDictProjectionMap.put(Words._ID, Words._ID); 89 sDictProjectionMap.put(Words.WORD, Words.WORD); 90 sDictProjectionMap.put(Words.FREQUENCY, Words.FREQUENCY); 91 sDictProjectionMap.put(Words.LOCALE, Words.LOCALE); 92 sDictProjectionMap.put(Words.APP_ID, Words.APP_ID); 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 105 DatabaseHelper(Context context) { 106 super(context, DATABASE_NAME, null, DATABASE_VERSION); 107 } 108 109 @Override 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 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 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 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 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 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 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 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 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 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