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