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