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