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