Home | History | Annotate | Download | only in dictionarypack
      1 /**
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.android.inputmethod.dictionarypack;
     18 
     19 import android.content.ContentProvider;
     20 import android.content.ContentResolver;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.UriMatcher;
     24 import android.content.res.AssetFileDescriptor;
     25 import android.database.AbstractCursor;
     26 import android.database.Cursor;
     27 import android.database.sqlite.SQLiteDatabase;
     28 import android.net.Uri;
     29 import android.os.ParcelFileDescriptor;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 
     33 import com.android.inputmethod.latin.R;
     34 
     35 import java.io.File;
     36 import java.io.FileNotFoundException;
     37 import java.util.Collection;
     38 import java.util.Collections;
     39 import java.util.HashMap;
     40 
     41 /**
     42  * Provider for dictionaries.
     43  *
     44  * This class is a ContentProvider exposing all available dictionary data as managed by
     45  * the dictionary pack.
     46  */
     47 public final class DictionaryProvider extends ContentProvider {
     48     private static final String TAG = DictionaryProvider.class.getSimpleName();
     49     public static final boolean DEBUG = false;
     50 
     51     public static final Uri CONTENT_URI =
     52             Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + DictionaryPackConstants.AUTHORITY);
     53     private static final String QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt";
     54     private static final String QUERY_PARAMETER_TRUE = "true";
     55     private static final String QUERY_PARAMETER_DELETE_RESULT = "result";
     56     private static final String QUERY_PARAMETER_SUCCESS = "success";
     57     private static final String QUERY_PARAMETER_FAILURE = "failure";
     58     public static final String QUERY_PARAMETER_PROTOCOL_VERSION = "protocol";
     59     private static final int NO_MATCH = 0;
     60     private static final int DICTIONARY_V1_WHOLE_LIST = 1;
     61     private static final int DICTIONARY_V1_DICT_INFO = 2;
     62     private static final int DICTIONARY_V2_METADATA = 3;
     63     private static final int DICTIONARY_V2_WHOLE_LIST = 4;
     64     private static final int DICTIONARY_V2_DICT_INFO = 5;
     65     private static final int DICTIONARY_V2_DATAFILE = 6;
     66     private static final UriMatcher sUriMatcherV1 = new UriMatcher(NO_MATCH);
     67     private static final UriMatcher sUriMatcherV2 = new UriMatcher(NO_MATCH);
     68     static
     69     {
     70         sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "list", DICTIONARY_V1_WHOLE_LIST);
     71         sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "*", DICTIONARY_V1_DICT_INFO);
     72         sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/metadata",
     73                 DICTIONARY_V2_METADATA);
     74         sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/list", DICTIONARY_V2_WHOLE_LIST);
     75         sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/dict/*",
     76                 DICTIONARY_V2_DICT_INFO);
     77         sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/datafile/*",
     78                 DICTIONARY_V2_DATAFILE);
     79     }
     80 
     81     // MIME types for dictionary and dictionary list, as required by ContentProvider contract.
     82     public static final String DICT_LIST_MIME_TYPE =
     83             "vnd.android.cursor.item/vnd.google.dictionarylist";
     84     public static final String DICT_DATAFILE_MIME_TYPE =
     85             "vnd.android.cursor.item/vnd.google.dictionary";
     86 
     87     public static final String ID_CATEGORY_SEPARATOR = ":";
     88 
     89     private static final class WordListInfo {
     90         public final String mId;
     91         public final String mLocale;
     92         public final int mMatchLevel;
     93         public WordListInfo(final String id, final String locale, final int matchLevel) {
     94             mId = id;
     95             mLocale = locale;
     96             mMatchLevel = matchLevel;
     97         }
     98     }
     99 
    100     /**
    101      * A cursor for returning a list of file ids from a List of strings.
    102      *
    103      * This simulates only the necessary methods. It has no error handling to speak of,
    104      * and does not support everything a database does, only a few select necessary methods.
    105      */
    106     private static final class ResourcePathCursor extends AbstractCursor {
    107 
    108         // Column names for the cursor returned by this content provider.
    109         static private final String[] columnNames = { "id", "locale" };
    110 
    111         // The list of word lists served by this provider that match the client request.
    112         final WordListInfo[] mWordLists;
    113         // Note : the cursor also uses mPos, which is defined in AbstractCursor.
    114 
    115         public ResourcePathCursor(final Collection<WordListInfo> wordLists) {
    116             // Allocating a 0-size WordListInfo here allows the toArray() method
    117             // to ensure we have a strongly-typed array. It's thrown out. That's
    118             // what the documentation of #toArray says to do in order to get a
    119             // new strongly typed array of the correct size.
    120             mWordLists = wordLists.toArray(new WordListInfo[0]);
    121             mPos = 0;
    122         }
    123 
    124         @Override
    125         public String[] getColumnNames() {
    126             return columnNames;
    127         }
    128 
    129         @Override
    130         public int getCount() {
    131             return mWordLists.length;
    132         }
    133 
    134         @Override public double getDouble(int column) { return 0; }
    135         @Override public float getFloat(int column) { return 0; }
    136         @Override public int getInt(int column) { return 0; }
    137         @Override public short getShort(int column) { return 0; }
    138         @Override public long getLong(int column) { return 0; }
    139 
    140         @Override public String getString(final int column) {
    141             switch (column) {
    142                 case 0: return mWordLists[mPos].mId;
    143                 case 1: return mWordLists[mPos].mLocale;
    144                 default : return null;
    145             }
    146         }
    147 
    148         @Override
    149         public boolean isNull(final int column) {
    150             if (mPos >= mWordLists.length) return true;
    151             return column != 0;
    152         }
    153     }
    154 
    155     @Override
    156     public boolean onCreate() {
    157         return true;
    158     }
    159 
    160     private static int matchUri(final Uri uri) {
    161         int protocolVersion = 1;
    162         final String protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION);
    163         if ("2".equals(protocolVersionArg)) protocolVersion = 2;
    164         switch (protocolVersion) {
    165             case 1: return sUriMatcherV1.match(uri);
    166             case 2: return sUriMatcherV2.match(uri);
    167             default: return NO_MATCH;
    168         }
    169     }
    170 
    171     private static String getClientId(final Uri uri) {
    172         int protocolVersion = 1;
    173         final String protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION);
    174         if ("2".equals(protocolVersionArg)) protocolVersion = 2;
    175         switch (protocolVersion) {
    176             case 1: return null; // In protocol 1, the client ID is always null.
    177             case 2: return uri.getPathSegments().get(0);
    178             default: return null;
    179         }
    180     }
    181 
    182     /**
    183      * Returns the MIME type of the content associated with an Uri
    184      *
    185      * @see android.content.ContentProvider#getType(android.net.Uri)
    186      *
    187      * @param uri the URI of the content the type of which should be returned.
    188      * @return the MIME type, or null if the URL is not recognized.
    189      */
    190     @Override
    191     public String getType(final Uri uri) {
    192         PrivateLog.log("Asked for type of : " + uri);
    193         final int match = matchUri(uri);
    194         switch (match) {
    195             case NO_MATCH: return null;
    196             case DICTIONARY_V1_WHOLE_LIST:
    197             case DICTIONARY_V1_DICT_INFO:
    198             case DICTIONARY_V2_WHOLE_LIST:
    199             case DICTIONARY_V2_DICT_INFO: return DICT_LIST_MIME_TYPE;
    200             case DICTIONARY_V2_DATAFILE: return DICT_DATAFILE_MIME_TYPE;
    201             default: return null;
    202         }
    203     }
    204 
    205     /**
    206      * Query the provider for dictionary files.
    207      *
    208      * This version dispatches the query according to the protocol version found in the
    209      * ?protocol= query parameter. If absent or not well-formed, it defaults to 1.
    210      * @see android.content.ContentProvider#query(Uri, String[], String, String[], String)
    211      *
    212      * @param uri a content uri (see sUriMatcherV{1,2} at the top of this file for format)
    213      * @param projection ignored. All columns are always returned.
    214      * @param selection ignored.
    215      * @param selectionArgs ignored.
    216      * @param sortOrder ignored. The results are always returned in no particular order.
    217      * @return a cursor matching the uri, or null if the URI was not recognized.
    218      */
    219     @Override
    220     public Cursor query(final Uri uri, final String[] projection, final String selection,
    221             final String[] selectionArgs, final String sortOrder) {
    222         Utils.l("Uri =", uri);
    223         PrivateLog.log("Query : " + uri);
    224         final String clientId = getClientId(uri);
    225         final int match = matchUri(uri);
    226         switch (match) {
    227             case DICTIONARY_V1_WHOLE_LIST:
    228             case DICTIONARY_V2_WHOLE_LIST:
    229                 final Cursor c = MetadataDbHelper.queryDictionaries(getContext(), clientId);
    230                 Utils.l("List of dictionaries with count", c.getCount());
    231                 PrivateLog.log("Returned a list of " + c.getCount() + " items");
    232                 return c;
    233             case DICTIONARY_V2_DICT_INFO:
    234                 // In protocol version 2, we return null if the client is unknown. Otherwise
    235                 // we behave exactly like for protocol 1.
    236                 if (!MetadataDbHelper.isClientKnown(getContext(), clientId)) return null;
    237                 // Fall through
    238             case DICTIONARY_V1_DICT_INFO:
    239                 final String locale = uri.getLastPathSegment();
    240                 // If LatinIME does not have a dictionary for this locale at all, it will
    241                 // send us true for this value. In this case, we may prompt the user for
    242                 // a decision about downloading a dictionary even over a metered connection.
    243                 final String mayPromptValue =
    244                         uri.getQueryParameter(QUERY_PARAMETER_MAY_PROMPT_USER);
    245                 final boolean mayPrompt = QUERY_PARAMETER_TRUE.equals(mayPromptValue);
    246                 final Collection<WordListInfo> dictFiles =
    247                         getDictionaryWordListsForLocale(clientId, locale, mayPrompt);
    248                 // TODO: pass clientId to the following function
    249                 DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext());
    250                 if (null != dictFiles && dictFiles.size() > 0) {
    251                     PrivateLog.log("Returned " + dictFiles.size() + " files");
    252                     return new ResourcePathCursor(dictFiles);
    253                 } else {
    254                     PrivateLog.log("No dictionary files for this URL");
    255                     return new ResourcePathCursor(Collections.<WordListInfo>emptyList());
    256                 }
    257             // V2_METADATA and V2_DATAFILE are not supported for query()
    258             default:
    259                 return null;
    260         }
    261     }
    262 
    263     /**
    264      * Helper method to get the wordlist metadata associated with a wordlist ID.
    265      *
    266      * @param clientId the ID of the client
    267      * @param wordlistId the ID of the wordlist for which to get the metadata.
    268      * @return the metadata for this wordlist ID, or null if none could be found.
    269      */
    270     private ContentValues getWordlistMetadataForWordlistId(final String clientId,
    271             final String wordlistId) {
    272         final Context context = getContext();
    273         if (TextUtils.isEmpty(wordlistId)) return null;
    274         final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
    275         return MetadataDbHelper.getInstalledOrDeletingWordListContentValuesByWordListId(
    276                 db, wordlistId);
    277     }
    278 
    279     /**
    280      * Opens an asset file for an URI.
    281      *
    282      * Called by {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} or
    283      * {@link android.content.ContentResolver#openInputStream(Uri)} from a client requesting a
    284      * dictionary.
    285      * @see android.content.ContentProvider#openAssetFile(Uri, String)
    286      *
    287      * @param uri the URI the file is for.
    288      * @param mode the mode to read the file. MUST be "r" for readonly.
    289      * @return the descriptor, or null if the file is not found or if mode is not equals to "r".
    290      */
    291     @Override
    292     public AssetFileDescriptor openAssetFile(final Uri uri, final String mode) {
    293         if (null == mode || !"r".equals(mode)) return null;
    294 
    295         final int match = matchUri(uri);
    296         if (DICTIONARY_V1_DICT_INFO != match && DICTIONARY_V2_DATAFILE != match) {
    297             // Unsupported URI for openAssetFile
    298             Log.w(TAG, "Unsupported URI for openAssetFile : " + uri);
    299             return null;
    300         }
    301         final String wordlistId = uri.getLastPathSegment();
    302         final String clientId = getClientId(uri);
    303         final ContentValues wordList = getWordlistMetadataForWordlistId(clientId, wordlistId);
    304 
    305         if (null == wordList) return null;
    306 
    307         try {
    308             final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
    309             if (MetadataDbHelper.STATUS_DELETING == status) {
    310                 // This will return an empty file (R.raw.empty points at an empty dictionary)
    311                 // This is how we "delete" the files. It allows Android Keyboard to fake deleting
    312                 // a default dictionary - which is actually in its assets and can't be really
    313                 // deleted.
    314                 final AssetFileDescriptor afd = getContext().getResources().openRawResourceFd(
    315                         R.raw.empty);
    316                 return afd;
    317             } else {
    318                 final String localFilename =
    319                         wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
    320                 final File f = getContext().getFileStreamPath(localFilename);
    321                 final ParcelFileDescriptor pfd =
    322                         ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
    323                 return new AssetFileDescriptor(pfd, 0, pfd.getStatSize());
    324             }
    325         } catch (FileNotFoundException e) {
    326             // No file : fall through and return null
    327         }
    328         return null;
    329     }
    330 
    331     /**
    332      * Reads the metadata and returns the collection of dictionaries for a given locale.
    333      *
    334      * Word list IDs are expected to be in the form category:manual_id. This method
    335      * will select only one word list for each category: the one with the most specific
    336      * locale matching the locale specified in the URI. The manual id serves only to
    337      * distinguish a word list from another for the purpose of updating, and is arbitrary
    338      * but may not contain a colon.
    339      *
    340      * @param clientId the ID of the client requesting the list
    341      * @param locale the locale for which we want the list, as a String
    342      * @param mayPrompt true if we are allowed to prompt the user for arbitration via notification
    343      * @return a collection of ids. It is guaranteed to be non-null, but may be empty.
    344      */
    345     private Collection<WordListInfo> getDictionaryWordListsForLocale(final String clientId,
    346             final String locale, final boolean mayPrompt) {
    347         final Context context = getContext();
    348         final Cursor results =
    349                 MetadataDbHelper.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context,
    350                         clientId);
    351         if (null == results) {
    352             return Collections.<WordListInfo>emptyList();
    353         } else {
    354             final HashMap<String, WordListInfo> dicts = new HashMap<String, WordListInfo>();
    355             final int idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
    356             final int localeIndex = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
    357             final int localFileNameIndex =
    358                     results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
    359             final int statusIndex = results.getColumnIndex(MetadataDbHelper.STATUS_COLUMN);
    360             if (results.moveToFirst()) {
    361                 do {
    362                     final String wordListId = results.getString(idIndex);
    363                     if (TextUtils.isEmpty(wordListId)) continue;
    364                     final String[] wordListIdArray =
    365                             TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR);
    366                     final String wordListCategory;
    367                     if (2 == wordListIdArray.length) {
    368                         // This is at the category:manual_id format.
    369                         wordListCategory = wordListIdArray[0];
    370                         // We don't need to read wordListIdArray[1] here, because it's irrelevant to
    371                         // word list selection - it's just a name we use to identify which data file
    372                         // is a newer version of which word list. We do however return the full id
    373                         // string for each selected word list, so in this sense we are 'using' it.
    374                     } else {
    375                         // This does not contain a colon, like the old format does. Old-format IDs
    376                         // always point to main dictionaries, so we force the main category upon it.
    377                         wordListCategory = UpdateHandler.MAIN_DICTIONARY_CATEGORY;
    378                     }
    379                     final String wordListLocale = results.getString(localeIndex);
    380                     final String wordListLocalFilename = results.getString(localFileNameIndex);
    381                     final int wordListStatus = results.getInt(statusIndex);
    382                     // Test the requested locale against this wordlist locale. The requested locale
    383                     // has to either match exactly or be more specific than the dictionary - a
    384                     // dictionary for "en" would match both a request for "en" or for "en_US", but a
    385                     // dictionary for "en_GB" would not match a request for "en_US". Thus if all
    386                     // three of "en" "en_US" and "en_GB" dictionaries are installed, a request for
    387                     // "en_US" would match "en" and "en_US", and a request for "en" only would only
    388                     // match the generic "en" dictionary. For more details, see the documentation
    389                     // for LocaleUtils#getMatchLevel.
    390                     final int matchLevel = LocaleUtils.getMatchLevel(wordListLocale, locale);
    391                     if (!LocaleUtils.isMatch(matchLevel)) {
    392                         // The locale of this wordlist does not match the required locale.
    393                         // Skip this wordlist and go to the next.
    394                         continue;
    395                     }
    396                     if (MetadataDbHelper.STATUS_INSTALLED == wordListStatus) {
    397                         // If the file does not exist, it has been deleted and the IME should
    398                         // already have it. Do not return it. However, this only applies if the
    399                         // word list is INSTALLED, for if it is DELETING we should return it always
    400                         // so that Android Keyboard can perform the actual deletion.
    401                         final File f = getContext().getFileStreamPath(wordListLocalFilename);
    402                         if (!f.isFile()) {
    403                             continue;
    404                         }
    405                     } else if (MetadataDbHelper.STATUS_AVAILABLE == wordListStatus) {
    406                         // The locale is the id for the main dictionary.
    407                         UpdateHandler.installIfNeverRequested(context, clientId, wordListId,
    408                                 mayPrompt);
    409                         continue;
    410                     }
    411                     final WordListInfo currentBestMatch = dicts.get(wordListCategory);
    412                     if (null == currentBestMatch
    413                             || currentBestMatch.mMatchLevel < matchLevel) {
    414                         dicts.put(wordListCategory,
    415                                 new WordListInfo(wordListId, wordListLocale, matchLevel));
    416                     }
    417                 } while (results.moveToNext());
    418             }
    419             results.close();
    420             return Collections.unmodifiableCollection(dicts.values());
    421         }
    422     }
    423 
    424     /**
    425      * Deletes the file pointed by Uri, as returned by openAssetFile.
    426      *
    427      * @param uri the URI the file is for.
    428      * @param selection ignored
    429      * @param selectionArgs ignored
    430      * @return the number of files deleted (0 or 1 in the current implementation)
    431      * @see android.content.ContentProvider#delete(Uri, String, String[])
    432      */
    433     @Override
    434     public int delete(final Uri uri, final String selection, final String[] selectionArgs)
    435             throws UnsupportedOperationException {
    436         final int match = matchUri(uri);
    437         if (DICTIONARY_V1_DICT_INFO == match || DICTIONARY_V2_DATAFILE == match) {
    438             return deleteDataFile(uri);
    439         }
    440         if (DICTIONARY_V2_METADATA == match) {
    441             if (MetadataDbHelper.deleteClient(getContext(), getClientId(uri))) {
    442                 return 1;
    443             }
    444             return 0;
    445         }
    446         // Unsupported URI for delete
    447         return 0;
    448     }
    449 
    450     private int deleteDataFile(final Uri uri) {
    451         final String wordlistId = uri.getLastPathSegment();
    452         final String clientId = getClientId(uri);
    453         final ContentValues wordList = getWordlistMetadataForWordlistId(clientId, wordlistId);
    454         if (null == wordList) return 0;
    455         final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
    456         final int version = wordList.getAsInteger(MetadataDbHelper.VERSION_COLUMN);
    457         if (MetadataDbHelper.STATUS_DELETING == status) {
    458             UpdateHandler.markAsDeleted(getContext(), clientId, wordlistId, version, status);
    459             return 1;
    460         } else if (MetadataDbHelper.STATUS_INSTALLED == status) {
    461             final String result = uri.getQueryParameter(QUERY_PARAMETER_DELETE_RESULT);
    462             if (QUERY_PARAMETER_FAILURE.equals(result)) {
    463                 UpdateHandler.markAsBroken(getContext(), clientId, wordlistId, version);
    464             }
    465             final String localFilename =
    466                     wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
    467             final File f = getContext().getFileStreamPath(localFilename);
    468             // f.delete() returns true if the file was successfully deleted, false otherwise
    469             if (f.delete()) {
    470                 return 1;
    471             } else {
    472                 return 0;
    473             }
    474         } else {
    475             Log.e(TAG, "Attempt to delete a file whose status is " + status);
    476             return 0;
    477         }
    478     }
    479 
    480     /**
    481      * Insert data into the provider. May be either a metadata source URL or some dictionary info.
    482      *
    483      * @param uri the designated content URI. See sUriMatcherV{1,2} for available URIs.
    484      * @param values the values to insert for this content uri
    485      * @return the URI for the newly inserted item. May be null if arguments don't allow for insert
    486      */
    487     @Override
    488     public Uri insert(final Uri uri, final ContentValues values)
    489             throws UnsupportedOperationException {
    490         if (null == uri || null == values) return null; // Should never happen but let's be safe
    491         PrivateLog.log("Insert, uri = " + uri.toString());
    492         final String clientId = getClientId(uri);
    493         switch (matchUri(uri)) {
    494             case DICTIONARY_V2_METADATA:
    495                 // The values should contain a valid client ID and a valid URI for the metadata.
    496                 // The client ID may not be null, nor may it be empty because the empty client ID
    497                 // is reserved for internal use.
    498                 // The metadata URI may not be null, but it may be empty if the client does not
    499                 // want the dictionary pack to update the metadata automatically.
    500                 MetadataDbHelper.updateClientInfo(getContext(), clientId, values);
    501                 break;
    502             case DICTIONARY_V2_DICT_INFO:
    503                 try {
    504                     final WordListMetadata newDictionaryMetadata =
    505                             WordListMetadata.createFromContentValues(
    506                                     MetadataDbHelper.completeWithDefaultValues(values));
    507                     new ActionBatch.MarkPreInstalledAction(clientId, newDictionaryMetadata)
    508                             .execute(getContext());
    509                 } catch (final BadFormatException e) {
    510                     Log.w(TAG, "Not enough information to insert this dictionary " + values, e);
    511                 }
    512                 // We just received new information about the list of dictionary for this client.
    513                 // For all intents and purposes, this is new metadata, so we should publish it
    514                 // so that any listeners (like the Settings interface for example) can update
    515                 // themselves.
    516                 UpdateHandler.publishUpdateMetadataCompleted(getContext(), true);
    517                 break;
    518             case DICTIONARY_V1_WHOLE_LIST:
    519             case DICTIONARY_V1_DICT_INFO:
    520                 PrivateLog.log("Attempt to insert : " + uri);
    521                 throw new UnsupportedOperationException(
    522                         "Insertion in the dictionary is not supported in this version");
    523         }
    524         return uri;
    525     }
    526 
    527     /**
    528      * Updating data is not supported, and will throw an exception.
    529      * @see android.content.ContentProvider#update(Uri, ContentValues, String, String[])
    530      * @see android.content.ContentProvider#insert(Uri, ContentValues)
    531      */
    532     @Override
    533     public int update(final Uri uri, final ContentValues values, final String selection,
    534             final String[] selectionArgs) throws UnsupportedOperationException {
    535         PrivateLog.log("Attempt to update : " + uri);
    536         throw new UnsupportedOperationException("Updating dictionary words is not supported");
    537     }
    538 }
    539