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