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