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