• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     {
sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "list", DICTIONARY_V1_WHOLE_LIST)70         sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "list", DICTIONARY_V1_WHOLE_LIST);
sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "*", DICTIONARY_V1_DICT_INFO)71         sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "*", DICTIONARY_V1_DICT_INFO);
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/metadata", DICTIONARY_V2_METADATA)72         sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/metadata",
73                 DICTIONARY_V2_METADATA);
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/list", DICTIONARY_V2_WHOLE_LIST)74         sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/list", DICTIONARY_V2_WHOLE_LIST);
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/dict/*", DICTIONARY_V2_DICT_INFO)75         sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/dict/*",
76                 DICTIONARY_V2_DICT_INFO);
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/datafile/*", DICTIONARY_V2_DATAFILE)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;
WordListInfo(final String id, final String locale, final String rawChecksum, final int matchLevel)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 
ResourcePathCursor(final Collection<WordListInfo> wordLists)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
getColumnNames()129         public String[] getColumnNames() {
130             return columnNames;
131         }
132 
133         @Override
getCount()134         public int getCount() {
135             return mWordLists.length;
136         }
137 
getDouble(int column)138         @Override public double getDouble(int column) { return 0; }
getFloat(int column)139         @Override public float getFloat(int column) { return 0; }
getInt(int column)140         @Override public int getInt(int column) { return 0; }
getShort(int column)141         @Override public short getShort(int column) { return 0; }
getLong(int column)142         @Override public long getLong(int column) { return 0; }
143 
getString(final int column)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
isNull(final int column)154         public boolean isNull(final int column) {
155             if (mPos >= mWordLists.length) return true;
156             return column != 0;
157         }
158     }
159 
160     @Override
onCreate()161     public boolean onCreate() {
162         return true;
163     }
164 
matchUri(final Uri uri)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 
getClientId(final Uri uri)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
getType(final Uri uri)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
query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder)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      */
getWordlistMetadataForWordlistId(final String clientId, final String wordlistId)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
openAssetFile(final Uri uri, final String mode)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      */
getDictionaryWordListsForLocale(final String clientId, final String locale, final boolean mayPrompt)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
delete(final Uri uri, final String selection, final String[] selectionArgs)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 
deleteDataFile(final Uri uri)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
insert(final Uri uri, final ContentValues values)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
update(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs)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