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