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.app.DownloadManager; 20 import android.app.DownloadManager.Query; 21 import android.app.DownloadManager.Request; 22 import android.app.Notification; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.SharedPreferences; 29 import android.content.res.Resources; 30 import android.database.Cursor; 31 import android.database.sqlite.SQLiteDatabase; 32 import android.net.ConnectivityManager; 33 import android.net.Uri; 34 import android.os.Build; 35 import android.os.ParcelFileDescriptor; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import com.android.inputmethod.compat.ConnectivityManagerCompatUtils; 40 import com.android.inputmethod.compat.DownloadManagerCompatUtils; 41 import com.android.inputmethod.compat.NotificationCompatUtils; 42 import com.android.inputmethod.latin.R; 43 import com.android.inputmethod.latin.utils.ApplicationUtils; 44 import com.android.inputmethod.latin.utils.DebugLogUtils; 45 46 import java.io.File; 47 import java.io.FileInputStream; 48 import java.io.FileNotFoundException; 49 import java.io.FileOutputStream; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.io.InputStreamReader; 53 import java.io.OutputStream; 54 import java.nio.channels.FileChannel; 55 import java.util.ArrayList; 56 import java.util.Collections; 57 import java.util.LinkedList; 58 import java.util.List; 59 import java.util.Locale; 60 import java.util.Set; 61 import java.util.TreeSet; 62 63 /** 64 * Handler for the update process. 65 * 66 * This class is in charge of coordinating the update process for the various dictionaries 67 * stored in the dictionary pack. 68 */ 69 public final class UpdateHandler { 70 static final String TAG = "DictionaryProvider:" + UpdateHandler.class.getSimpleName(); 71 private static final boolean DEBUG = DictionaryProvider.DEBUG; 72 73 // Used to prevent trying to read the id of the downloaded file before it is written 74 static final Object sSharedIdProtector = new Object(); 75 76 // Value used to mean this is not a real DownloadManager downloaded file id 77 // DownloadManager uses as an ID numbers returned out of an AUTOINCREMENT column 78 // in SQLite, so it should never return anything < 0. 79 public static final int NOT_AN_ID = -1; 80 public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION = 2; 81 82 // Arbitrary. Probably good if it's a power of 2, and a couple thousand bytes long. 83 private static final int FILE_COPY_BUFFER_SIZE = 8192; 84 85 // Table fixed values for metadata / downloads 86 final static String METADATA_NAME = "metadata"; 87 final static int METADATA_TYPE = 0; 88 final static int WORDLIST_TYPE = 1; 89 90 // Suffix for generated dictionary files 91 private static final String DICT_FILE_SUFFIX = ".dict"; 92 // Name of the category for the main dictionary 93 public static final String MAIN_DICTIONARY_CATEGORY = "main"; 94 95 // The id for the "dictionary available" notification. 96 static final int DICT_AVAILABLE_NOTIFICATION_ID = 1; 97 98 /** 99 * An interface for UIs or services that want to know when something happened. 100 * 101 * This is chiefly used by the dictionary manager UI. 102 */ 103 public interface UpdateEventListener { 104 public void downloadedMetadata(boolean succeeded); 105 public void wordListDownloadFinished(String wordListId, boolean succeeded); 106 public void updateCycleCompleted(); 107 } 108 109 /** 110 * The list of currently registered listeners. 111 */ 112 private static List<UpdateEventListener> sUpdateEventListeners 113 = Collections.synchronizedList(new LinkedList<UpdateEventListener>()); 114 115 /** 116 * Register a new listener to be notified of updates. 117 * 118 * Don't forget to call unregisterUpdateEventListener when done with it, or 119 * it will leak the register. 120 */ 121 public static void registerUpdateEventListener(final UpdateEventListener listener) { 122 sUpdateEventListeners.add(listener); 123 } 124 125 /** 126 * Unregister a previously registered listener. 127 */ 128 public static void unregisterUpdateEventListener(final UpdateEventListener listener) { 129 sUpdateEventListeners.remove(listener); 130 } 131 132 private static final String DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY = "downloadOverMetered"; 133 134 /** 135 * Write the DownloadManager ID of the currently downloading metadata to permanent storage. 136 * 137 * @param context to open shared prefs 138 * @param uri the uri of the metadata 139 * @param downloadId the id returned by DownloadManager 140 */ 141 private static void writeMetadataDownloadId(final Context context, final String uri, 142 final long downloadId) { 143 MetadataDbHelper.registerMetadataDownloadId(context, uri, downloadId); 144 } 145 146 public static final int DOWNLOAD_OVER_METERED_SETTING_UNKNOWN = 0; 147 public static final int DOWNLOAD_OVER_METERED_ALLOWED = 1; 148 public static final int DOWNLOAD_OVER_METERED_DISALLOWED = 2; 149 150 /** 151 * Sets the setting that tells us whether we may download over a metered connection. 152 */ 153 public static void setDownloadOverMeteredSetting(final Context context, 154 final boolean shouldDownloadOverMetered) { 155 final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); 156 final SharedPreferences.Editor editor = prefs.edit(); 157 editor.putInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, shouldDownloadOverMetered 158 ? DOWNLOAD_OVER_METERED_ALLOWED : DOWNLOAD_OVER_METERED_DISALLOWED); 159 editor.apply(); 160 } 161 162 /** 163 * Gets the setting that tells us whether we may download over a metered connection. 164 * 165 * This returns one of the constants above. 166 */ 167 public static int getDownloadOverMeteredSetting(final Context context) { 168 final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); 169 final int setting = prefs.getInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, 170 DOWNLOAD_OVER_METERED_SETTING_UNKNOWN); 171 return setting; 172 } 173 174 /** 175 * Download latest metadata from the server through DownloadManager for all known clients 176 * @param context The context for retrieving resources 177 * @param updateNow Whether we should update NOW, or respect bandwidth policies 178 * @return true if an update successfully started, false otherwise. 179 */ 180 public static boolean tryUpdate(final Context context, final boolean updateNow) { 181 // TODO: loop through all clients instead of only doing the default one. 182 final TreeSet<String> uris = new TreeSet<>(); 183 final Cursor cursor = MetadataDbHelper.queryClientIds(context); 184 if (null == cursor) return false; 185 try { 186 if (!cursor.moveToFirst()) return false; 187 do { 188 final String clientId = cursor.getString(0); 189 final String metadataUri = 190 MetadataDbHelper.getMetadataUriAsString(context, clientId); 191 PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId)); 192 DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri); 193 uris.add(metadataUri); 194 } while (cursor.moveToNext()); 195 } finally { 196 cursor.close(); 197 } 198 boolean started = false; 199 for (final String metadataUri : uris) { 200 if (!TextUtils.isEmpty(metadataUri)) { 201 // If the metadata URI is empty, that means we should never update it at all. 202 // It should not be possible to come here with a null metadata URI, because 203 // it should have been rejected at the time of client registration; if there 204 // is a bug and it happens anyway, doing nothing is the right thing to do. 205 // For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}. 206 updateClientsWithMetadataUri(context, updateNow, metadataUri); 207 started = true; 208 } 209 } 210 return started; 211 } 212 213 /** 214 * Download latest metadata from the server through DownloadManager for all relevant clients 215 * 216 * @param context The context for retrieving resources 217 * @param updateNow Whether we should update NOW, or respect bandwidth policies 218 * @param metadataUri The client to update 219 */ 220 private static void updateClientsWithMetadataUri(final Context context, 221 final boolean updateNow, final String metadataUri) { 222 PrivateLog.log("Update for metadata URI " + DebugLogUtils.s(metadataUri)); 223 // Adding a disambiguator to circumvent a bug in older versions of DownloadManager. 224 // DownloadManager also stupidly cuts the extension to replace with its own that it 225 // gets from the content-type. We need to circumvent this. 226 final String disambiguator = "#" + System.currentTimeMillis() 227 + ApplicationUtils.getVersionName(context) + ".json"; 228 final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator)); 229 DebugLogUtils.l("Request =", metadataRequest); 230 231 final Resources res = context.getResources(); 232 // By default, download over roaming is allowed and all network types are allowed too. 233 if (!updateNow) { 234 final boolean allowedOverMetered = res.getBoolean(R.bool.allow_over_metered); 235 // If we don't have to update NOW, then only do it over non-metered connections. 236 if (DownloadManagerCompatUtils.hasSetAllowedOverMetered()) { 237 DownloadManagerCompatUtils.setAllowedOverMetered(metadataRequest, 238 allowedOverMetered); 239 } else if (!allowedOverMetered) { 240 metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI); 241 } 242 metadataRequest.setAllowedOverRoaming(res.getBoolean(R.bool.allow_over_roaming)); 243 } 244 final boolean notificationVisible = updateNow 245 ? res.getBoolean(R.bool.display_notification_for_user_requested_update) 246 : res.getBoolean(R.bool.display_notification_for_auto_update); 247 248 metadataRequest.setTitle(res.getString(R.string.download_description)); 249 metadataRequest.setNotificationVisibility(notificationVisible 250 ? Request.VISIBILITY_VISIBLE : Request.VISIBILITY_HIDDEN); 251 metadataRequest.setVisibleInDownloadsUi( 252 res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); 253 254 final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); 255 cancelUpdateWithDownloadManager(context, metadataUri, manager); 256 final long downloadId; 257 synchronized (sSharedIdProtector) { 258 downloadId = manager.enqueue(metadataRequest); 259 DebugLogUtils.l("Metadata download requested with id", downloadId); 260 // If there is already a download in progress, it's been there for a while and 261 // there is probably something wrong with download manager. It's best to just 262 // overwrite the id and request it again. If the old one happens to finish 263 // anyway, we don't know about its ID any more, so the downloadFinished 264 // method will ignore it. 265 writeMetadataDownloadId(context, metadataUri, downloadId); 266 } 267 PrivateLog.log("Requested download with id " + downloadId); 268 } 269 270 /** 271 * Cancels downloading a file, if there is one for this URI. 272 * 273 * If we are not currently downloading the file at this URI, this is a no-op. 274 * 275 * @param context the context to open the database on 276 * @param metadataUri the URI to cancel 277 * @param manager an wrapped instance of DownloadManager 278 */ 279 private static void cancelUpdateWithDownloadManager(final Context context, 280 final String metadataUri, final DownloadManagerWrapper manager) { 281 synchronized (sSharedIdProtector) { 282 final long metadataDownloadId = 283 MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri); 284 if (NOT_AN_ID == metadataDownloadId) return; 285 manager.remove(metadataDownloadId); 286 writeMetadataDownloadId(context, metadataUri, NOT_AN_ID); 287 } 288 // Consider a cancellation as a failure. As such, inform listeners that the download 289 // has failed. 290 for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { 291 listener.downloadedMetadata(false); 292 } 293 } 294 295 /** 296 * Cancels a pending update for this client, if there is one. 297 * 298 * If we are not currently updating metadata for this client, this is a no-op. This is a helper 299 * method that gets the download manager service and the metadata URI for this client. 300 * 301 * @param context the context, to get an instance of DownloadManager 302 * @param clientId the ID of the client we want to cancel the update of 303 */ 304 public static void cancelUpdate(final Context context, final String clientId) { 305 final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); 306 final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId); 307 cancelUpdateWithDownloadManager(context, metadataUri, manager); 308 } 309 310 /** 311 * Registers a download request and flags it as downloading in the metadata table. 312 * 313 * This is a helper method that exists to avoid race conditions where DownloadManager might 314 * finish downloading the file before the data is committed to the database. 315 * It registers the request with the DownloadManager service and also updates the metadata 316 * database directly within a synchronized section. 317 * This method has no intelligence about the data it commits to the database aside from the 318 * download request id, which is not known before submitting the request to the download 319 * manager. Hence, it only updates the relevant line. 320 * 321 * @param manager a wrapped download manager service to register the request with. 322 * @param request the request to register. 323 * @param db the metadata database. 324 * @param id the id of the word list. 325 * @param version the version of the word list. 326 * @return the download id returned by the download manager. 327 */ 328 public static long registerDownloadRequest(final DownloadManagerWrapper manager, 329 final Request request, final SQLiteDatabase db, final String id, final int version) { 330 DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version); 331 final long downloadId; 332 synchronized (sSharedIdProtector) { 333 downloadId = manager.enqueue(request); 334 DebugLogUtils.l("Download requested with id", downloadId); 335 MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId); 336 } 337 return downloadId; 338 } 339 340 /** 341 * Retrieve information about a specific download from DownloadManager. 342 */ 343 private static CompletedDownloadInfo getCompletedDownloadInfo( 344 final DownloadManagerWrapper manager, final long downloadId) { 345 final Query query = new Query().setFilterById(downloadId); 346 final Cursor cursor = manager.query(query); 347 348 if (null == cursor) { 349 return new CompletedDownloadInfo(null, downloadId, DownloadManager.STATUS_FAILED); 350 } 351 try { 352 final String uri; 353 final int status; 354 if (cursor.moveToNext()) { 355 final int columnStatus = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); 356 final int columnError = cursor.getColumnIndex(DownloadManager.COLUMN_REASON); 357 final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI); 358 final int error = cursor.getInt(columnError); 359 status = cursor.getInt(columnStatus); 360 final String uriWithAnchor = cursor.getString(columnUri); 361 int anchorIndex = uriWithAnchor.indexOf('#'); 362 if (anchorIndex != -1) { 363 uri = uriWithAnchor.substring(0, anchorIndex); 364 } else { 365 uri = uriWithAnchor; 366 } 367 if (DownloadManager.STATUS_SUCCESSFUL != status) { 368 Log.e(TAG, "Permanent failure of download " + downloadId 369 + " with error code: " + error); 370 } 371 } else { 372 uri = null; 373 status = DownloadManager.STATUS_FAILED; 374 } 375 return new CompletedDownloadInfo(uri, downloadId, status); 376 } finally { 377 cursor.close(); 378 } 379 } 380 381 private static ArrayList<DownloadRecord> getDownloadRecordsForCompletedDownloadInfo( 382 final Context context, final CompletedDownloadInfo downloadInfo) { 383 // Get and check the ID of the file we are waiting for, compare them to downloaded ones 384 synchronized(sSharedIdProtector) { 385 final ArrayList<DownloadRecord> downloadRecords = 386 MetadataDbHelper.getDownloadRecordsForDownloadId(context, 387 downloadInfo.mDownloadId); 388 // If any of these is metadata, we should update the DB 389 boolean hasMetadata = false; 390 for (DownloadRecord record : downloadRecords) { 391 if (null == record.mAttributes) { 392 hasMetadata = true; 393 break; 394 } 395 } 396 if (hasMetadata) { 397 writeMetadataDownloadId(context, downloadInfo.mUri, NOT_AN_ID); 398 MetadataDbHelper.saveLastUpdateTimeOfUri(context, downloadInfo.mUri); 399 } 400 return downloadRecords; 401 } 402 } 403 404 /** 405 * Take appropriate action after a download finished, in success or in error. 406 * 407 * This is called by the system upon broadcast from the DownloadManager that a file 408 * has been downloaded successfully. 409 * After a simple check that this is actually the file we are waiting for, this 410 * method basically coordinates the parsing and comparison of metadata, and fires 411 * the computation of the list of actions that should be taken then executes them. 412 * 413 * @param context The context for this action. 414 * @param intent The intent from the DownloadManager containing details about the download. 415 */ 416 /* package */ static void downloadFinished(final Context context, final Intent intent) { 417 // Get and check the ID of the file that was downloaded 418 final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID); 419 PrivateLog.log("Download finished with id " + fileId); 420 DebugLogUtils.l("DownloadFinished with id", fileId); 421 if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore 422 423 final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); 424 final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId); 425 426 final ArrayList<DownloadRecord> recordList = 427 getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo); 428 if (null == recordList) return; // It was someone else's download. 429 DebugLogUtils.l("Received result for download ", fileId); 430 431 // TODO: handle gracefully a null pointer here. This is practically impossible because 432 // we come here only when DownloadManager explicitly called us when it ended a 433 // download, so we are pretty sure it's alive. It's theoretically possible that it's 434 // disabled right inbetween the firing of the intent and the control reaching here. 435 436 for (final DownloadRecord record : recordList) { 437 // downloadSuccessful is not final because we may still have exceptions from now on 438 boolean downloadSuccessful = false; 439 try { 440 if (downloadInfo.wasSuccessful()) { 441 downloadSuccessful = handleDownloadedFile(context, record, manager, fileId); 442 } 443 } finally { 444 if (record.isMetadata()) { 445 publishUpdateMetadataCompleted(context, downloadSuccessful); 446 } else { 447 final SQLiteDatabase db = MetadataDbHelper.getDb(context, record.mClientId); 448 publishUpdateWordListCompleted(context, downloadSuccessful, fileId, 449 db, record.mAttributes, record.mClientId); 450 } 451 } 452 } 453 // Now that we're done using it, we can remove this download from DLManager 454 manager.remove(fileId); 455 } 456 457 /** 458 * Sends a broadcast informing listeners that the dictionaries were updated. 459 * 460 * This will call all local listeners through the UpdateEventListener#downloadedMetadata 461 * callback (for example, the dictionary provider interface uses this to stop the Loading 462 * animation) and send a broadcast about the metadata having been updated. For a client of 463 * the dictionary pack like Latin IME, this means it should re-query the dictionary pack 464 * for any relevant new data. 465 * 466 * @param context the context, to send the broadcast. 467 * @param downloadSuccessful whether the download of the metadata was successful or not. 468 */ 469 public static void publishUpdateMetadataCompleted(final Context context, 470 final boolean downloadSuccessful) { 471 // We need to warn all listeners of what happened. But some listeners may want to 472 // remove themselves or re-register something in response. Hence we should take a 473 // snapshot of the listener list and warn them all. This also prevents any 474 // concurrent modification problem of the static list. 475 for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { 476 listener.downloadedMetadata(downloadSuccessful); 477 } 478 publishUpdateCycleCompletedEvent(context); 479 } 480 481 private static void publishUpdateWordListCompleted(final Context context, 482 final boolean downloadSuccessful, final long fileId, 483 final SQLiteDatabase db, final ContentValues downloadedFileRecord, 484 final String clientId) { 485 synchronized(sSharedIdProtector) { 486 if (downloadSuccessful) { 487 final ActionBatch actions = new ActionBatch(); 488 actions.add(new ActionBatch.InstallAfterDownloadAction(clientId, 489 downloadedFileRecord)); 490 actions.execute(context, new LogProblemReporter(TAG)); 491 } else { 492 MetadataDbHelper.deleteDownloadingEntry(db, fileId); 493 } 494 } 495 // See comment above about #linkedCopyOfLists 496 for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { 497 listener.wordListDownloadFinished(downloadedFileRecord.getAsString( 498 MetadataDbHelper.WORDLISTID_COLUMN), downloadSuccessful); 499 } 500 publishUpdateCycleCompletedEvent(context); 501 } 502 503 private static void publishUpdateCycleCompletedEvent(final Context context) { 504 // Even if this is not successful, we have to publish the new state. 505 PrivateLog.log("Publishing update cycle completed event"); 506 DebugLogUtils.l("Publishing update cycle completed event"); 507 for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { 508 listener.updateCycleCompleted(); 509 } 510 signalNewDictionaryState(context); 511 } 512 513 private static boolean handleDownloadedFile(final Context context, 514 final DownloadRecord downloadRecord, final DownloadManagerWrapper manager, 515 final long fileId) { 516 try { 517 // {@link handleWordList(Context,InputStream,ContentValues)}. 518 // Handle the downloaded file according to its type 519 if (downloadRecord.isMetadata()) { 520 DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId); 521 // #handleMetadata() closes its InputStream argument 522 handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream( 523 manager.openDownloadedFile(fileId)), downloadRecord.mClientId); 524 } else { 525 DebugLogUtils.l("Data D/L'd is a word list"); 526 final int wordListStatus = downloadRecord.mAttributes.getAsInteger( 527 MetadataDbHelper.STATUS_COLUMN); 528 if (MetadataDbHelper.STATUS_DOWNLOADING == wordListStatus) { 529 // #handleWordList() closes its InputStream argument 530 handleWordList(context, new ParcelFileDescriptor.AutoCloseInputStream( 531 manager.openDownloadedFile(fileId)), downloadRecord); 532 } else { 533 Log.e(TAG, "Spurious download ended. Maybe a cancelled download?"); 534 } 535 } 536 return true; 537 } catch (FileNotFoundException e) { 538 Log.e(TAG, "A file was downloaded but it can't be opened", e); 539 } catch (IOException e) { 540 // Can't read the file... disk damage? 541 Log.e(TAG, "Can't read a file", e); 542 // TODO: Check with UX how we should warn the user. 543 } catch (IllegalStateException e) { 544 // The format of the downloaded file is incorrect. We should maybe report upstream? 545 Log.e(TAG, "Incorrect data received", e); 546 } catch (BadFormatException e) { 547 // The format of the downloaded file is incorrect. We should maybe report upstream? 548 Log.e(TAG, "Incorrect data received", e); 549 } 550 return false; 551 } 552 553 /** 554 * Returns a copy of the specified list, with all elements copied. 555 * 556 * This returns a linked list. 557 */ 558 private static <T> List<T> linkedCopyOfList(final List<T> src) { 559 // Instantiation of a parameterized type is not possible in Java, so it's not possible to 560 // return the same type of list that was passed - probably the same reason why Collections 561 // does not do it. So we need to decide statically which concrete type to return. 562 return new LinkedList<>(src); 563 } 564 565 /** 566 * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data. 567 */ 568 private static void signalNewDictionaryState(final Context context) { 569 final Intent newDictBroadcast = 570 new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); 571 context.sendBroadcast(newDictBroadcast); 572 } 573 574 /** 575 * Parse metadata and take appropriate action (that is, upgrade dictionaries). 576 * @param context the context to read settings. 577 * @param stream an input stream pointing to the downloaded data. May not be null. 578 * Will be closed upon finishing. 579 * @param clientId the ID of the client to update 580 * @throws BadFormatException if the metadata is not in a known format. 581 * @throws IOException if the downloaded file can't be read from the disk 582 */ 583 private static void handleMetadata(final Context context, final InputStream stream, 584 final String clientId) throws IOException, BadFormatException { 585 DebugLogUtils.l("Entering handleMetadata"); 586 final List<WordListMetadata> newMetadata; 587 final InputStreamReader reader = new InputStreamReader(stream); 588 try { 589 // According to the doc InputStreamReader buffers, so no need to add a buffering layer 590 newMetadata = MetadataHandler.readMetadata(reader); 591 } finally { 592 reader.close(); 593 } 594 595 DebugLogUtils.l("Downloaded metadata :", newMetadata); 596 PrivateLog.log("Downloaded metadata\n" + newMetadata); 597 598 final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata); 599 // TODO: Check with UX how we should report to the user 600 // TODO: add an action to close the database 601 actions.execute(context, new LogProblemReporter(TAG)); 602 } 603 604 /** 605 * Handle a word list: put it in its right place, and update the passed content values. 606 * @param context the context for opening files. 607 * @param inputStream an input stream pointing to the downloaded data. May not be null. 608 * Will be closed upon finishing. 609 * @param downloadRecord the content values to fill the file name in. 610 * @throws IOException if files can't be read or written. 611 * @throws BadFormatException if the md5 checksum doesn't match the metadata. 612 */ 613 private static void handleWordList(final Context context, 614 final InputStream inputStream, final DownloadRecord downloadRecord) 615 throws IOException, BadFormatException { 616 617 // DownloadManager does not have the ability to put the file directly where we want 618 // it, so we had it download to a temporary place. Now we move it. It will be deleted 619 // automatically by DownloadManager. 620 DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString( 621 MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId); 622 PrivateLog.log("Downloaded a new word list with description : " 623 + downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN) 624 + " for " + downloadRecord.mClientId); 625 626 final String locale = 627 downloadRecord.mAttributes.getAsString(MetadataDbHelper.LOCALE_COLUMN); 628 final String destinationFile = getTempFileName(context, locale); 629 downloadRecord.mAttributes.put(MetadataDbHelper.LOCAL_FILENAME_COLUMN, destinationFile); 630 631 FileOutputStream outputStream = null; 632 try { 633 outputStream = context.openFileOutput(destinationFile, Context.MODE_PRIVATE); 634 copyFile(inputStream, outputStream); 635 } finally { 636 inputStream.close(); 637 if (outputStream != null) { 638 outputStream.close(); 639 } 640 } 641 642 // TODO: Consolidate this MD5 calculation with file copying above. 643 // We need to reopen the file because the inputstream bytes have been consumed, and there 644 // is nothing in InputStream to reopen or rewind the stream 645 FileInputStream copiedFile = null; 646 final String md5sum; 647 try { 648 copiedFile = context.openFileInput(destinationFile); 649 md5sum = MD5Calculator.checksum(copiedFile); 650 } finally { 651 if (copiedFile != null) { 652 copiedFile.close(); 653 } 654 } 655 if (TextUtils.isEmpty(md5sum)) { 656 return; // We can't compute the checksum anyway, so return and hope for the best 657 } 658 if (!md5sum.equals(downloadRecord.mAttributes.getAsString( 659 MetadataDbHelper.CHECKSUM_COLUMN))) { 660 context.deleteFile(destinationFile); 661 throw new BadFormatException("MD5 checksum check failed : \"" + md5sum + "\" <> \"" 662 + downloadRecord.mAttributes.getAsString(MetadataDbHelper.CHECKSUM_COLUMN) 663 + "\""); 664 } 665 } 666 667 /** 668 * Copies in to out using FileChannels. 669 * 670 * This tries to use channels for fast copying. If it doesn't work, fall back to 671 * copyFileFallBack below. 672 * 673 * @param in the stream to copy from. 674 * @param out the stream to copy to. 675 * @throws IOException if both the normal and fallback methods raise exceptions. 676 */ 677 private static void copyFile(final InputStream in, final OutputStream out) 678 throws IOException { 679 DebugLogUtils.l("Copying files"); 680 if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) { 681 DebugLogUtils.l("Not the right types"); 682 copyFileFallback(in, out); 683 } else { 684 try { 685 final FileChannel sourceChannel = ((FileInputStream) in).getChannel(); 686 final FileChannel destinationChannel = ((FileOutputStream) out).getChannel(); 687 sourceChannel.transferTo(0, Integer.MAX_VALUE, destinationChannel); 688 } catch (IOException e) { 689 // Can't work with channels, or something went wrong. Copy by hand. 690 DebugLogUtils.l("Won't work"); 691 copyFileFallback(in, out); 692 } 693 } 694 } 695 696 /** 697 * Copies in to out with read/write methods, not FileChannels. 698 * 699 * @param in the stream to copy from. 700 * @param out the stream to copy to. 701 * @throws IOException if a read or a write fails. 702 */ 703 private static void copyFileFallback(final InputStream in, final OutputStream out) 704 throws IOException { 705 DebugLogUtils.l("Falling back to slow copy"); 706 final byte[] buffer = new byte[FILE_COPY_BUFFER_SIZE]; 707 for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer)) 708 out.write(buffer, 0, readBytes); 709 } 710 711 /** 712 * Creates and returns a new file to store a dictionary 713 * @param context the context to use to open the file. 714 * @param locale the locale for this dictionary, to make the file name more readable. 715 * @return the file name, or throw an exception. 716 * @throws IOException if the file cannot be created. 717 */ 718 private static String getTempFileName(final Context context, final String locale) 719 throws IOException { 720 DebugLogUtils.l("Entering openTempFileOutput"); 721 final File dir = context.getFilesDir(); 722 final File f = File.createTempFile(locale + "___", DICT_FILE_SUFFIX, dir); 723 DebugLogUtils.l("File name is", f.getName()); 724 return f.getName(); 725 } 726 727 /** 728 * Compare metadata (collections of word lists). 729 * 730 * This method takes whole metadata sets directly and compares them, matching the wordlists in 731 * each of them on the id. It creates an ActionBatch object that can be .execute()'d to perform 732 * the actual upgrade from `from' to `to'. 733 * 734 * @param context the context to open databases on. 735 * @param clientId the id of the client. 736 * @param from the dictionary descriptor (as a list of wordlists) to upgrade from. 737 * @param to the dictionary descriptor (as a list of wordlists) to upgrade to. 738 * @return an ordered list of runnables to be called to upgrade. 739 */ 740 private static ActionBatch compareMetadataForUpgrade(final Context context, 741 final String clientId, List<WordListMetadata> from, List<WordListMetadata> to) { 742 final ActionBatch actions = new ActionBatch(); 743 // Upgrade existing word lists 744 DebugLogUtils.l("Comparing dictionaries"); 745 final Set<String> wordListIds = new TreeSet<>(); 746 // TODO: Can these be null? 747 if (null == from) from = new ArrayList<>(); 748 if (null == to) to = new ArrayList<>(); 749 for (WordListMetadata wlData : from) wordListIds.add(wlData.mId); 750 for (WordListMetadata wlData : to) wordListIds.add(wlData.mId); 751 for (String id : wordListIds) { 752 final WordListMetadata currentInfo = MetadataHandler.findWordListById(from, id); 753 final WordListMetadata metadataInfo = MetadataHandler.findWordListById(to, id); 754 // TODO: Remove the following unnecessary check, since we are now doing the filtering 755 // inside findWordListById. 756 final WordListMetadata newInfo = null == metadataInfo 757 || metadataInfo.mFormatVersion > MAXIMUM_SUPPORTED_FORMAT_VERSION 758 ? null : metadataInfo; 759 DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo); 760 761 if (null == currentInfo && null == newInfo) { 762 // This may happen if a new word list appeared that we can't handle. 763 if (null == metadataInfo) { 764 // What happened? Bug in Set<>? 765 Log.e(TAG, "Got an id for a wordlist that is neither in from nor in to"); 766 } else { 767 // We may come here if there is a new word list that we can't handle. 768 Log.i(TAG, "Can't handle word list with id '" + id + "' because it has format" 769 + " version " + metadataInfo.mFormatVersion + " and the maximum version" 770 + " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION); 771 } 772 continue; 773 } else if (null == currentInfo) { 774 // This is the case where a new list that we did not know of popped on the server. 775 // Make it available. 776 actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); 777 } else if (null == newInfo) { 778 // This is the case where an old list we had is not in the server data any more. 779 // Pass false to ForgetAction: this may be installed and we still want to apply 780 // a forget-like action (remove the URL) if it is, so we want to turn off the 781 // status == AVAILABLE check. If it's DELETING, this is the right thing to do, 782 // as we want to leave the record as long as Android Keyboard has not deleted it ; 783 // the record will be removed when the file is actually deleted. 784 actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, false)); 785 } else { 786 final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); 787 if (newInfo.mVersion == currentInfo.mVersion) { 788 // If it's the same id/version, we update the DB with the new values. 789 // It doesn't matter too much if they didn't change. 790 actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo)); 791 } else if (newInfo.mVersion > currentInfo.mVersion) { 792 // If it's a new version, it's a different entry in the database. Make it 793 // available, and if it's installed, also start the download. 794 final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, 795 currentInfo.mId, currentInfo.mVersion); 796 final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); 797 actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); 798 if (status == MetadataDbHelper.STATUS_INSTALLED 799 || status == MetadataDbHelper.STATUS_DISABLED) { 800 actions.add(new ActionBatch.StartDownloadAction(clientId, newInfo, false)); 801 } else { 802 // Pass true to ForgetAction: this is indeed an update to a non-installed 803 // word list, so activate status == AVAILABLE check 804 // In case the status is DELETING, this is the right thing to do. It will 805 // leave the entry as DELETING and remove its URL so that Android Keyboard 806 // can delete it the next time it starts up. 807 actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, true)); 808 } 809 } else if (DEBUG) { 810 Log.i(TAG, "Not updating word list " + id 811 + " : current list timestamp is " + currentInfo.mLastUpdate 812 + " ; new list timestamp is " + newInfo.mLastUpdate); 813 } 814 } 815 } 816 return actions; 817 } 818 819 /** 820 * Computes an upgrade from the current state of the dictionaries to some desired state. 821 * @param context the context for reading settings and files. 822 * @param clientId the id of the client. 823 * @param newMetadata the state we want to upgrade to. 824 * @return the upgrade from the current state to the desired state, ready to be executed. 825 */ 826 public static ActionBatch computeUpgradeTo(final Context context, final String clientId, 827 final List<WordListMetadata> newMetadata) { 828 final List<WordListMetadata> currentMetadata = 829 MetadataHandler.getCurrentMetadata(context, clientId); 830 return compareMetadataForUpgrade(context, clientId, currentMetadata, newMetadata); 831 } 832 833 /** 834 * Shows the notification that informs the user a dictionary is available. 835 * 836 * When this notification is clicked, the dialog for downloading the dictionary 837 * over a metered connection is shown. 838 */ 839 private static void showDictionaryAvailableNotification(final Context context, 840 final String clientId, final ContentValues installCandidate) { 841 final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); 842 final Intent intent = new Intent(); 843 intent.setClass(context, DownloadOverMeteredDialog.class); 844 intent.putExtra(DownloadOverMeteredDialog.CLIENT_ID_KEY, clientId); 845 intent.putExtra(DownloadOverMeteredDialog.WORDLIST_TO_DOWNLOAD_KEY, 846 installCandidate.getAsString(MetadataDbHelper.WORDLISTID_COLUMN)); 847 intent.putExtra(DownloadOverMeteredDialog.SIZE_KEY, 848 installCandidate.getAsInteger(MetadataDbHelper.FILESIZE_COLUMN)); 849 intent.putExtra(DownloadOverMeteredDialog.LOCALE_KEY, localeString); 850 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 851 final PendingIntent notificationIntent = PendingIntent.getActivity(context, 852 0 /* requestCode */, intent, 853 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT); 854 final NotificationManager notificationManager = 855 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 856 // None of those are expected to happen, but just in case... 857 if (null == notificationIntent || null == notificationManager) return; 858 859 final Locale locale = LocaleUtils.constructLocaleFromString(localeString); 860 final String language = (null == locale ? "" : locale.getDisplayLanguage()); 861 final String titleFormat = context.getString(R.string.dict_available_notification_title); 862 final String notificationTitle = String.format(titleFormat, language); 863 final Notification.Builder builder = new Notification.Builder(context) 864 .setAutoCancel(true) 865 .setContentIntent(notificationIntent) 866 .setContentTitle(notificationTitle) 867 .setContentText(context.getString(R.string.dict_available_notification_description)) 868 .setTicker(notificationTitle) 869 .setOngoing(false) 870 .setOnlyAlertOnce(true) 871 .setSmallIcon(R.drawable.ic_notify_dictionary); 872 NotificationCompatUtils.setColor(builder, 873 context.getResources().getColor(R.color.notification_accent_color)); 874 NotificationCompatUtils.setPriorityToLow(builder); 875 NotificationCompatUtils.setVisibilityToSecret(builder); 876 NotificationCompatUtils.setCategoryToRecommendation(builder); 877 final Notification notification = NotificationCompatUtils.build(builder); 878 notificationManager.notify(DICT_AVAILABLE_NOTIFICATION_ID, notification); 879 } 880 881 /** 882 * Installs a word list if it has never been requested. 883 * 884 * This is called when a word list is requested, and is available but not installed. It checks 885 * the conditions for auto-installation: if the dictionary is a main dictionary for this 886 * language, and it has never been opted out through the dictionary interface, then we start 887 * installing it. For the user who enables a language and uses it for the first time, the 888 * dictionary should magically start being used a short time after they start typing. 889 * The mayPrompt argument indicates whether we should prompt the user for a decision to 890 * download or not, in case we decide we are in the case where we should download - this 891 * roughly happens when the current connectivity is 3G. See 892 * DictionaryProvider#getDictionaryWordListsForContentUri for details. 893 */ 894 // As opposed to many other methods, this method does not need the version of the word 895 // list because it may only install the latest version we know about for this specific 896 // word list ID / client ID combination. 897 public static void installIfNeverRequested(final Context context, final String clientId, 898 final String wordlistId, final boolean mayPrompt) { 899 final String[] idArray = wordlistId.split(DictionaryProvider.ID_CATEGORY_SEPARATOR); 900 // If we have a new-format dictionary id (category:manual_id), then use the 901 // specified category. Otherwise, it is a main dictionary, so force the 902 // MAIN category upon it. 903 final String category = 2 == idArray.length ? idArray[0] : MAIN_DICTIONARY_CATEGORY; 904 if (!MAIN_DICTIONARY_CATEGORY.equals(category)) { 905 // Not a main dictionary. We only auto-install main dictionaries, so we can return now. 906 return; 907 } 908 if (CommonPreferences.getCommonPreferences(context).contains(wordlistId)) { 909 // If some kind of settings has been done in the past for this specific id, then 910 // this is not a candidate for auto-install. Because it already is either true, 911 // in which case it may be installed or downloading or whatever, and we don't 912 // need to care about it because it's already handled or being handled, or it's false 913 // in which case it means the user explicitely turned it off and don't want to have 914 // it installed. So we quit right away. 915 return; 916 } 917 918 final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); 919 final ContentValues installCandidate = 920 MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId); 921 if (MetadataDbHelper.STATUS_AVAILABLE 922 != installCandidate.getAsInteger(MetadataDbHelper.STATUS_COLUMN)) { 923 // If it's not "AVAILABLE", we want to stop now. Because candidates for auto-install 924 // are lists that we know are available, but we also know have never been installed. 925 // It does obviously not concern already installed lists, or downloading lists, 926 // or those that have been disabled, flagged as deleting... So anything else than 927 // AVAILABLE means we don't auto-install. 928 return; 929 } 930 931 if (mayPrompt 932 && DOWNLOAD_OVER_METERED_SETTING_UNKNOWN 933 == getDownloadOverMeteredSetting(context)) { 934 final ConnectivityManager cm = 935 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 936 if (ConnectivityManagerCompatUtils.isActiveNetworkMetered(cm)) { 937 showDictionaryAvailableNotification(context, clientId, installCandidate); 938 return; 939 } 940 } 941 942 // We decided against prompting the user for a decision. This may be because we were 943 // explicitly asked not to, or because we are currently on wi-fi anyway, or because we 944 // already know the answer to the question. We'll enqueue a request ; StartDownloadAction 945 // knows to use the correct type of network according to the current settings. 946 947 // Also note that once it's auto-installed, a word list will be marked as INSTALLED. It will 948 // thus receive automatic updates if there are any, which is what we want. If the user does 949 // not want this word list, they will have to go to the settings and change them, which will 950 // change the shared preferences. So there is no way for a word list that has been 951 // auto-installed once to get auto-installed again, and that's what we want. 952 final ActionBatch actions = new ActionBatch(); 953 actions.add(new ActionBatch.StartDownloadAction(clientId, 954 WordListMetadata.createFromContentValues(installCandidate), false)); 955 final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); 956 // We are in a content provider: we can't do any UI at all. We have to defer the displaying 957 // itself to the service. Also, we only display this when the user does not have a 958 // dictionary for this language already: we know that from the mayPrompt argument. 959 if (mayPrompt) { 960 final Intent intent = new Intent(); 961 intent.setClass(context, DictionaryService.class); 962 intent.setAction(DictionaryService.SHOW_DOWNLOAD_TOAST_INTENT_ACTION); 963 intent.putExtra(DictionaryService.LOCALE_INTENT_ARGUMENT, localeString); 964 context.startService(intent); 965 } 966 actions.execute(context, new LogProblemReporter(TAG)); 967 } 968 969 /** 970 * Marks the word list with the passed id as used. 971 * 972 * This will download/install the list as required. The action will see that the destination 973 * word list is a valid list, and take appropriate action - in this case, mark it as used. 974 * @see ActionBatch.Action#execute 975 * 976 * @param context the context for using action batches. 977 * @param clientId the id of the client. 978 * @param wordlistId the id of the word list to mark as installed. 979 * @param version the version of the word list to mark as installed. 980 * @param status the current status of the word list. 981 * @param allowDownloadOnMeteredData whether to download even on metered data connection 982 */ 983 // The version argument is not used yet, because we don't need it to retrieve the information 984 // we need. However, the pair (id, version) being the primary key to a word list in the database 985 // it feels better for consistency to pass it, and some methods retrieving information about a 986 // word list need it so we may need it in the future. 987 public static void markAsUsed(final Context context, final String clientId, 988 final String wordlistId, final int version, 989 final int status, final boolean allowDownloadOnMeteredData) { 990 final List<WordListMetadata> currentMetadata = 991 MetadataHandler.getCurrentMetadata(context, clientId); 992 WordListMetadata wordList = MetadataHandler.findWordListById(currentMetadata, wordlistId); 993 if (null == wordList) return; 994 final ActionBatch actions = new ActionBatch(); 995 if (MetadataDbHelper.STATUS_DISABLED == status 996 || MetadataDbHelper.STATUS_DELETING == status) { 997 actions.add(new ActionBatch.EnableAction(clientId, wordList)); 998 } else if (MetadataDbHelper.STATUS_AVAILABLE == status) { 999 actions.add(new ActionBatch.StartDownloadAction(clientId, wordList, 1000 allowDownloadOnMeteredData)); 1001 } else { 1002 Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status); 1003 } 1004 actions.execute(context, new LogProblemReporter(TAG)); 1005 signalNewDictionaryState(context); 1006 } 1007 1008 /** 1009 * Marks the word list with the passed id as unused. 1010 * 1011 * This leaves the file on the disk for ulterior use. The action will see that the destination 1012 * word list is null, and take appropriate action - in this case, mark it as unused. 1013 * @see ActionBatch.Action#execute 1014 * 1015 * @param context the context for using action batches. 1016 * @param clientId the id of the client. 1017 * @param wordlistId the id of the word list to mark as installed. 1018 * @param version the version of the word list to mark as installed. 1019 * @param status the current status of the word list. 1020 */ 1021 // The version and status arguments are not used yet, but this method matches its interface to 1022 // markAsUsed for consistency. 1023 public static void markAsUnused(final Context context, final String clientId, 1024 final String wordlistId, final int version, final int status) { 1025 final List<WordListMetadata> currentMetadata = 1026 MetadataHandler.getCurrentMetadata(context, clientId); 1027 final WordListMetadata wordList = 1028 MetadataHandler.findWordListById(currentMetadata, wordlistId); 1029 if (null == wordList) return; 1030 final ActionBatch actions = new ActionBatch(); 1031 actions.add(new ActionBatch.DisableAction(clientId, wordList)); 1032 actions.execute(context, new LogProblemReporter(TAG)); 1033 signalNewDictionaryState(context); 1034 } 1035 1036 /** 1037 * Marks the word list with the passed id as deleting. 1038 * 1039 * This basically means that on the next chance there is (right away if Android Keyboard 1040 * happens to be up, or the next time it gets up otherwise) the dictionary pack will 1041 * supply an empty dictionary to it that will replace whatever dictionary is installed. 1042 * This allows to release the space taken by a dictionary (except for the few bytes the 1043 * empty dictionary takes up), and override a built-in default dictionary so that we 1044 * can fake delete a built-in dictionary. 1045 * 1046 * @param context the context to open the database on. 1047 * @param clientId the id of the client. 1048 * @param wordlistId the id of the word list to mark as deleted. 1049 * @param version the version of the word list to mark as deleted. 1050 * @param status the current status of the word list. 1051 */ 1052 public static void markAsDeleting(final Context context, final String clientId, 1053 final String wordlistId, final int version, final int status) { 1054 final List<WordListMetadata> currentMetadata = 1055 MetadataHandler.getCurrentMetadata(context, clientId); 1056 final WordListMetadata wordList = 1057 MetadataHandler.findWordListById(currentMetadata, wordlistId); 1058 if (null == wordList) return; 1059 final ActionBatch actions = new ActionBatch(); 1060 actions.add(new ActionBatch.DisableAction(clientId, wordList)); 1061 actions.add(new ActionBatch.StartDeleteAction(clientId, wordList)); 1062 actions.execute(context, new LogProblemReporter(TAG)); 1063 signalNewDictionaryState(context); 1064 } 1065 1066 /** 1067 * Marks the word list with the passed id as actually deleted. 1068 * 1069 * This reverts to available status or deletes the row as appropriate. 1070 * 1071 * @param context the context to open the database on. 1072 * @param clientId the id of the client. 1073 * @param wordlistId the id of the word list to mark as deleted. 1074 * @param version the version of the word list to mark as deleted. 1075 * @param status the current status of the word list. 1076 */ 1077 public static void markAsDeleted(final Context context, final String clientId, 1078 final String wordlistId, final int version, final int status) { 1079 final List<WordListMetadata> currentMetadata = 1080 MetadataHandler.getCurrentMetadata(context, clientId); 1081 final WordListMetadata wordList = 1082 MetadataHandler.findWordListById(currentMetadata, wordlistId); 1083 if (null == wordList) return; 1084 final ActionBatch actions = new ActionBatch(); 1085 actions.add(new ActionBatch.FinishDeleteAction(clientId, wordList)); 1086 actions.execute(context, new LogProblemReporter(TAG)); 1087 signalNewDictionaryState(context); 1088 } 1089 1090 /** 1091 * Marks the word list with the passed id as broken. 1092 * 1093 * This effectively deletes the entry from the metadata. It doesn't prevent the same 1094 * word list to be downloaded again at a later time if the same or a new version is 1095 * available the next time we download the metadata. 1096 * 1097 * @param context the context to open the database on. 1098 * @param clientId the id of the client. 1099 * @param wordlistId the id of the word list to mark as broken. 1100 * @param version the version of the word list to mark as deleted. 1101 */ 1102 public static void markAsBroken(final Context context, final String clientId, 1103 final String wordlistId, final int version) { 1104 // TODO: do this on another thread to avoid blocking the UI. 1105 MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), 1106 wordlistId, version); 1107 } 1108 } 1109