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