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