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