Home | History | Annotate | Download | only in userdictionary
      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