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.Request; 20import android.content.ContentValues; 21import android.content.Context; 22import android.content.res.Resources; 23import android.database.sqlite.SQLiteDatabase; 24import android.net.Uri; 25import android.text.TextUtils; 26import android.util.Log; 27 28import com.android.inputmethod.latin.BinaryDictionaryFileDumper; 29import com.android.inputmethod.latin.R; 30import com.android.inputmethod.latin.common.LocaleUtils; 31import com.android.inputmethod.latin.utils.ApplicationUtils; 32import com.android.inputmethod.latin.utils.DebugLogUtils; 33 34import java.util.LinkedList; 35import java.util.Queue; 36 37/** 38 * Object representing an upgrade from one state to another. 39 * 40 * This implementation basically encapsulates a list of Runnable objects. In the future 41 * it may manage dependencies between them. Concretely, it does not use Runnable because the 42 * actions need an argument. 43 */ 44/* 45 46The state of a word list follows the following scheme. 47 48 | ^ 49 MakeAvailable | 50 | .------------Forget--------' 51 V | 52 STATUS_AVAILABLE <-------------------------. 53 | | 54StartDownloadAction FinishDeleteAction 55 | | 56 V | 57STATUS_DOWNLOADING EnableAction-- STATUS_DELETING 58 | | ^ 59InstallAfterDownloadAction | | 60 | .---------------' StartDeleteAction 61 | | | 62 V V | 63 STATUS_INSTALLED <--EnableAction-- STATUS_DISABLED 64 --DisableAction--> 65 66 It may also be possible that DisableAction or StartDeleteAction or 67 DownloadAction run when the file is still downloading. This cancels 68 the download and returns to STATUS_AVAILABLE. 69 Also, an UpdateDataAction may apply in any state. It does not affect 70 the state in any way (nor type, local filename, id or version) but 71 may update other attributes like description or remote filename. 72 73 Forget is an DB maintenance action that removes the entry if it is not installed or disabled. 74 This happens when the word list information disappeared from the server, or when a new version 75 is available and we should forget about the old one. 76*/ 77public final class ActionBatch { 78 /** 79 * A piece of update. 80 * 81 * Action is basically like a Runnable that takes an argument. 82 */ 83 public interface Action { 84 /** 85 * Execute this action NOW. 86 * @param context the context to get system services, resources, databases 87 */ 88 void execute(final Context context); 89 } 90 91 /** 92 * An action that starts downloading an available word list. 93 */ 94 public static final class StartDownloadAction implements Action { 95 static final String TAG = "DictionaryProvider:" + StartDownloadAction.class.getSimpleName(); 96 97 private final String mClientId; 98 // The data to download. May not be null. 99 final WordListMetadata mWordList; 100 public StartDownloadAction(final String clientId, final WordListMetadata wordList) { 101 DebugLogUtils.l("New download action for client ", clientId, " : ", wordList); 102 mClientId = clientId; 103 mWordList = wordList; 104 } 105 106 @Override 107 public void execute(final Context context) { 108 if (null == mWordList) { // This should never happen 109 Log.e(TAG, "UpdateAction with a null parameter!"); 110 return; 111 } 112 DebugLogUtils.l("Downloading word list"); 113 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 114 final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, 115 mWordList.mId, mWordList.mVersion); 116 final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); 117 final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); 118 if (MetadataDbHelper.STATUS_DOWNLOADING == status) { 119 // The word list is still downloading. Cancel the download and revert the 120 // word list status to "available". 121 manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); 122 MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); 123 } else if (MetadataDbHelper.STATUS_AVAILABLE != status 124 && MetadataDbHelper.STATUS_RETRYING != status) { 125 // Should never happen 126 Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " + status 127 + " for an upgrade action. Fall back to download."); 128 } 129 // Download it. 130 DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename); 131 132 // This is an upgraded word list: we should download it. 133 // Adding a disambiguator to circumvent a bug in older versions of DownloadManager. 134 // DownloadManager also stupidly cuts the extension to replace with its own that it 135 // gets from the content-type. We need to circumvent this. 136 final String disambiguator = "#" + System.currentTimeMillis() 137 + ApplicationUtils.getVersionName(context) + ".dict"; 138 final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator); 139 final Request request = new Request(uri); 140 141 final Resources res = context.getResources(); 142 request.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE); 143 request.setTitle(mWordList.mDescription); 144 request.setNotificationVisibility(Request.VISIBILITY_HIDDEN); 145 request.setVisibleInDownloadsUi( 146 res.getBoolean(R.bool.dict_downloads_visible_in_download_UI)); 147 148 final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db, 149 mWordList.mId, mWordList.mVersion); 150 Log.i(TAG, String.format("Starting the dictionary download with version:" 151 + " %d and Url: %s", mWordList.mVersion, uri)); 152 DebugLogUtils.l("Starting download of", uri, "with id", downloadId); 153 PrivateLog.log("Starting download of " + uri + ", id : " + downloadId); 154 } 155 } 156 157 /** 158 * An action that updates the database to reflect the status of a newly installed word list. 159 */ 160 public static final class InstallAfterDownloadAction implements Action { 161 static final String TAG = "DictionaryProvider:" 162 + InstallAfterDownloadAction.class.getSimpleName(); 163 private final String mClientId; 164 // The state to upgrade from. May not be null. 165 final ContentValues mWordListValues; 166 167 public InstallAfterDownloadAction(final String clientId, 168 final ContentValues wordListValues) { 169 DebugLogUtils.l("New InstallAfterDownloadAction for client ", clientId, " : ", 170 wordListValues); 171 mClientId = clientId; 172 mWordListValues = wordListValues; 173 } 174 175 @Override 176 public void execute(final Context context) { 177 if (null == mWordListValues) { 178 Log.e(TAG, "InstallAfterDownloadAction with a null parameter!"); 179 return; 180 } 181 final int status = mWordListValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN); 182 if (MetadataDbHelper.STATUS_DOWNLOADING != status) { 183 final String id = mWordListValues.getAsString(MetadataDbHelper.WORDLISTID_COLUMN); 184 Log.e(TAG, "Unexpected state of the word list '" + id + "' : " + status 185 + " for an InstallAfterDownload action. Bailing out."); 186 return; 187 } 188 189 DebugLogUtils.l("Setting word list as installed"); 190 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 191 MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues); 192 193 // Install the downloaded file by un-compressing and moving it to the staging 194 // directory. Ideally, we should do this before updating the DB, but the 195 // installDictToStagingFromContentProvider() relies on the db being updated. 196 final String localeString = mWordListValues.getAsString(MetadataDbHelper.LOCALE_COLUMN); 197 BinaryDictionaryFileDumper.installDictToStagingFromContentProvider( 198 LocaleUtils.constructLocaleFromString(localeString), context, false); 199 } 200 } 201 202 /** 203 * An action that enables an existing word list. 204 */ 205 public static final class EnableAction implements Action { 206 static final String TAG = "DictionaryProvider:" + EnableAction.class.getSimpleName(); 207 private final String mClientId; 208 // The state to upgrade from. May not be null. 209 final WordListMetadata mWordList; 210 211 public EnableAction(final String clientId, final WordListMetadata wordList) { 212 DebugLogUtils.l("New EnableAction for client ", clientId, " : ", wordList); 213 mClientId = clientId; 214 mWordList = wordList; 215 } 216 217 @Override 218 public void execute(final Context context) { 219 if (null == mWordList) { 220 Log.e(TAG, "EnableAction with a null parameter!"); 221 return; 222 } 223 DebugLogUtils.l("Enabling word list"); 224 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 225 final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, 226 mWordList.mId, mWordList.mVersion); 227 final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); 228 if (MetadataDbHelper.STATUS_DISABLED != status 229 && MetadataDbHelper.STATUS_DELETING != status) { 230 Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + " : " + status 231 + " for an enable action. Cancelling"); 232 return; 233 } 234 MetadataDbHelper.markEntryAsEnabled(db, mWordList.mId, mWordList.mVersion); 235 } 236 } 237 238 /** 239 * An action that disables a word list. 240 */ 241 public static final class DisableAction implements Action { 242 static final String TAG = "DictionaryProvider:" + DisableAction.class.getSimpleName(); 243 private final String mClientId; 244 // The word list to disable. May not be null. 245 final WordListMetadata mWordList; 246 public DisableAction(final String clientId, final WordListMetadata wordlist) { 247 DebugLogUtils.l("New Disable action for client ", clientId, " : ", wordlist); 248 mClientId = clientId; 249 mWordList = wordlist; 250 } 251 252 @Override 253 public void execute(final Context context) { 254 if (null == mWordList) { // This should never happen 255 Log.e(TAG, "DisableAction with a null word list!"); 256 return; 257 } 258 DebugLogUtils.l("Disabling word list : " + mWordList); 259 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 260 final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, 261 mWordList.mId, mWordList.mVersion); 262 final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); 263 if (MetadataDbHelper.STATUS_INSTALLED == status) { 264 // Disabling an installed word list 265 MetadataDbHelper.markEntryAsDisabled(db, mWordList.mId, mWordList.mVersion); 266 } else { 267 if (MetadataDbHelper.STATUS_DOWNLOADING != status) { 268 Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " 269 + status + " for a disable action. Fall back to marking as available."); 270 } 271 // The word list is still downloading. Cancel the download and revert the 272 // word list status to "available". 273 final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); 274 manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); 275 MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); 276 } 277 } 278 } 279 280 /** 281 * An action that makes a word list available. 282 */ 283 public static final class MakeAvailableAction implements Action { 284 static final String TAG = "DictionaryProvider:" + MakeAvailableAction.class.getSimpleName(); 285 private final String mClientId; 286 // The word list to make available. May not be null. 287 final WordListMetadata mWordList; 288 public MakeAvailableAction(final String clientId, final WordListMetadata wordlist) { 289 DebugLogUtils.l("New MakeAvailable action", clientId, " : ", wordlist); 290 mClientId = clientId; 291 mWordList = wordlist; 292 } 293 294 @Override 295 public void execute(final Context context) { 296 if (null == mWordList) { // This should never happen 297 Log.e(TAG, "MakeAvailableAction with a null word list!"); 298 return; 299 } 300 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 301 if (null != MetadataDbHelper.getContentValuesByWordListId(db, 302 mWordList.mId, mWordList.mVersion)) { 303 Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' " 304 + " for a makeavailable action. Marking as available anyway."); 305 } 306 DebugLogUtils.l("Making word list available : " + mWordList); 307 // If mLocalFilename is null, then it's a remote file that hasn't been downloaded 308 // yet, so we set the local filename to the empty string. 309 final ContentValues values = MetadataDbHelper.makeContentValues(0, 310 MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_AVAILABLE, 311 mWordList.mId, mWordList.mLocale, mWordList.mDescription, 312 null == mWordList.mLocalFilename ? "" : mWordList.mLocalFilename, 313 mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, 314 mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, 315 mWordList.mVersion, mWordList.mFormatVersion); 316 PrivateLog.log("Insert 'available' record for " + mWordList.mDescription 317 + " and locale " + mWordList.mLocale); 318 db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); 319 } 320 } 321 322 /** 323 * An action that marks a word list as pre-installed. 324 * 325 * This is almost the same as MakeAvailableAction, as it only inserts a line with parameters 326 * received from outside. 327 * Unlike MakeAvailableAction, the parameters are not received from a downloaded metadata file 328 * but from the client directly; it marks a word list as being "installed" and not "available". 329 * It also explicitly sets the filename to the empty string, so that we don't try to open 330 * it on our side. 331 */ 332 public static final class MarkPreInstalledAction implements Action { 333 static final String TAG = "DictionaryProvider:" 334 + MarkPreInstalledAction.class.getSimpleName(); 335 private final String mClientId; 336 // The word list to mark pre-installed. May not be null. 337 final WordListMetadata mWordList; 338 public MarkPreInstalledAction(final String clientId, final WordListMetadata wordlist) { 339 DebugLogUtils.l("New MarkPreInstalled action", clientId, " : ", wordlist); 340 mClientId = clientId; 341 mWordList = wordlist; 342 } 343 344 @Override 345 public void execute(final Context context) { 346 if (null == mWordList) { // This should never happen 347 Log.e(TAG, "MarkPreInstalledAction with a null word list!"); 348 return; 349 } 350 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 351 if (null != MetadataDbHelper.getContentValuesByWordListId(db, 352 mWordList.mId, mWordList.mVersion)) { 353 Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' " 354 + " for a markpreinstalled action. Marking as preinstalled anyway."); 355 } 356 DebugLogUtils.l("Marking word list preinstalled : " + mWordList); 357 // This word list is pre-installed : we don't have its file. We should reset 358 // the local file name to the empty string so that we don't try to open it 359 // accidentally. The remote filename may be set by the application if it so wishes. 360 final ContentValues values = MetadataDbHelper.makeContentValues(0, 361 MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED, 362 mWordList.mId, mWordList.mLocale, mWordList.mDescription, 363 TextUtils.isEmpty(mWordList.mLocalFilename) ? "" : mWordList.mLocalFilename, 364 mWordList.mRemoteFilename, mWordList.mLastUpdate, 365 mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mRetryCount, 366 mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion); 367 PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription 368 + " and locale " + mWordList.mLocale); 369 db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); 370 } 371 } 372 373 /** 374 * An action that updates information about a word list - description, locale etc 375 */ 376 public static final class UpdateDataAction implements Action { 377 static final String TAG = "DictionaryProvider:" + UpdateDataAction.class.getSimpleName(); 378 private final String mClientId; 379 final WordListMetadata mWordList; 380 public UpdateDataAction(final String clientId, final WordListMetadata wordlist) { 381 DebugLogUtils.l("New UpdateData action for client ", clientId, " : ", wordlist); 382 mClientId = clientId; 383 mWordList = wordlist; 384 } 385 386 @Override 387 public void execute(final Context context) { 388 if (null == mWordList) { // This should never happen 389 Log.e(TAG, "UpdateDataAction with a null word list!"); 390 return; 391 } 392 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 393 ContentValues oldValues = MetadataDbHelper.getContentValuesByWordListId(db, 394 mWordList.mId, mWordList.mVersion); 395 if (null == oldValues) { 396 Log.e(TAG, "Trying to update data about a non-existing word list. Bailing out."); 397 return; 398 } 399 DebugLogUtils.l("Updating data about a word list : " + mWordList); 400 final ContentValues values = MetadataDbHelper.makeContentValues( 401 oldValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN), 402 oldValues.getAsInteger(MetadataDbHelper.TYPE_COLUMN), 403 oldValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN), 404 mWordList.mId, mWordList.mLocale, mWordList.mDescription, 405 oldValues.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN), 406 mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, 407 mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, 408 mWordList.mVersion, mWordList.mFormatVersion); 409 PrivateLog.log("Updating record for " + mWordList.mDescription 410 + " and locale " + mWordList.mLocale); 411 db.update(MetadataDbHelper.METADATA_TABLE_NAME, values, 412 MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND " 413 + MetadataDbHelper.VERSION_COLUMN + " = ?", 414 new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) }); 415 } 416 } 417 418 /** 419 * An action that deletes the metadata about a word list if possible. 420 * 421 * This is triggered when a specific word list disappeared from the server, or when a fresher 422 * word list is available and the old one was not installed. 423 * If the word list has not been installed, it's possible to delete its associated metadata. 424 * Otherwise, the settings are retained so that the user can still administrate it. 425 */ 426 public static final class ForgetAction implements Action { 427 static final String TAG = "DictionaryProvider:" + ForgetAction.class.getSimpleName(); 428 private final String mClientId; 429 // The word list to remove. May not be null. 430 final WordListMetadata mWordList; 431 final boolean mHasNewerVersion; 432 public ForgetAction(final String clientId, final WordListMetadata wordlist, 433 final boolean hasNewerVersion) { 434 DebugLogUtils.l("New TryRemove action for client ", clientId, " : ", wordlist); 435 mClientId = clientId; 436 mWordList = wordlist; 437 mHasNewerVersion = hasNewerVersion; 438 } 439 440 @Override 441 public void execute(final Context context) { 442 if (null == mWordList) { // This should never happen 443 Log.e(TAG, "TryRemoveAction with a null word list!"); 444 return; 445 } 446 DebugLogUtils.l("Trying to remove word list : " + mWordList); 447 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 448 final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, 449 mWordList.mId, mWordList.mVersion); 450 if (null == values) { 451 Log.e(TAG, "Trying to update the metadata of a non-existing wordlist. Cancelling."); 452 return; 453 } 454 final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); 455 if (mHasNewerVersion && MetadataDbHelper.STATUS_AVAILABLE != status) { 456 // If we have a newer version of this word list, we should be here ONLY if it was 457 // not installed - else we should be upgrading it. 458 Log.e(TAG, "Unexpected status for forgetting a word list info : " + status 459 + ", removing URL to prevent re-download"); 460 } 461 if (MetadataDbHelper.STATUS_INSTALLED == status 462 || MetadataDbHelper.STATUS_DISABLED == status 463 || MetadataDbHelper.STATUS_DELETING == status) { 464 // If it is installed or disabled, we need to mark it as deleted so that LatinIME 465 // will remove it next time it enquires for dictionaries. 466 // If it is deleting and we don't have a new version, then we have to wait until 467 // LatinIME actually has deleted it before we can remove its metadata. 468 // In both cases, remove the URI from the database since it is not supposed to 469 // be accessible any more. 470 values.put(MetadataDbHelper.REMOTE_FILENAME_COLUMN, ""); 471 values.put(MetadataDbHelper.STATUS_COLUMN, MetadataDbHelper.STATUS_DELETING); 472 db.update(MetadataDbHelper.METADATA_TABLE_NAME, values, 473 MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND " 474 + MetadataDbHelper.VERSION_COLUMN + " = ?", 475 new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) }); 476 } else { 477 // If it's AVAILABLE or DOWNLOADING or even UNKNOWN, delete the entry. 478 db.delete(MetadataDbHelper.METADATA_TABLE_NAME, 479 MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND " 480 + MetadataDbHelper.VERSION_COLUMN + " = ?", 481 new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) }); 482 } 483 } 484 } 485 486 /** 487 * An action that sets the word list for deletion as soon as possible. 488 * 489 * This is triggered when the user requests deletion of a word list. This will mark it as 490 * deleted in the database, and fire an intent for Android Keyboard to take notice and 491 * reload its dictionaries right away if it is up. If it is not up now, then it will 492 * delete the actual file the next time it gets up. 493 * A file marked as deleted causes the content provider to supply a zero-sized file to 494 * Android Keyboard, which will overwrite any existing file and provide no words for this 495 * word list. This is not exactly a "deletion", since there is an actual file which takes up 496 * a few bytes on the disk, but this allows to override a default dictionary with an empty 497 * dictionary. This way, there is no need for the user to make a distinction between 498 * dictionaries installed by default and add-on dictionaries. 499 */ 500 public static final class StartDeleteAction implements Action { 501 static final String TAG = "DictionaryProvider:" + StartDeleteAction.class.getSimpleName(); 502 private final String mClientId; 503 // The word list to delete. May not be null. 504 final WordListMetadata mWordList; 505 public StartDeleteAction(final String clientId, final WordListMetadata wordlist) { 506 DebugLogUtils.l("New StartDelete action for client ", clientId, " : ", wordlist); 507 mClientId = clientId; 508 mWordList = wordlist; 509 } 510 511 @Override 512 public void execute(final Context context) { 513 if (null == mWordList) { // This should never happen 514 Log.e(TAG, "StartDeleteAction with a null word list!"); 515 return; 516 } 517 DebugLogUtils.l("Trying to delete word list : " + mWordList); 518 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 519 final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, 520 mWordList.mId, mWordList.mVersion); 521 if (null == values) { 522 Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling."); 523 return; 524 } 525 final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); 526 if (MetadataDbHelper.STATUS_DISABLED != status) { 527 Log.e(TAG, "Unexpected status for deleting a word list info : " + status); 528 } 529 MetadataDbHelper.markEntryAsDeleting(db, mWordList.mId, mWordList.mVersion); 530 } 531 } 532 533 /** 534 * An action that validates a word list as deleted. 535 * 536 * This will restore the word list as available if it still is, or remove the entry if 537 * it is not any more. 538 */ 539 public static final class FinishDeleteAction implements Action { 540 static final String TAG = "DictionaryProvider:" + FinishDeleteAction.class.getSimpleName(); 541 private final String mClientId; 542 // The word list to delete. May not be null. 543 final WordListMetadata mWordList; 544 public FinishDeleteAction(final String clientId, final WordListMetadata wordlist) { 545 DebugLogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist); 546 mClientId = clientId; 547 mWordList = wordlist; 548 } 549 550 @Override 551 public void execute(final Context context) { 552 if (null == mWordList) { // This should never happen 553 Log.e(TAG, "FinishDeleteAction with a null word list!"); 554 return; 555 } 556 DebugLogUtils.l("Trying to delete word list : " + mWordList); 557 final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId); 558 final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, 559 mWordList.mId, mWordList.mVersion); 560 if (null == values) { 561 Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling."); 562 return; 563 } 564 final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); 565 if (MetadataDbHelper.STATUS_DELETING != status) { 566 Log.e(TAG, "Unexpected status for finish-deleting a word list info : " + status); 567 } 568 final String remoteFilename = 569 values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN); 570 // If there isn't a remote filename any more, then we don't know where to get the file 571 // from any more, so we remove the entry entirely. As a matter of fact, if the file was 572 // marked DELETING but disappeared from the metadata on the server, it ended up 573 // this way. 574 if (TextUtils.isEmpty(remoteFilename)) { 575 db.delete(MetadataDbHelper.METADATA_TABLE_NAME, 576 MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND " 577 + MetadataDbHelper.VERSION_COLUMN + " = ?", 578 new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) }); 579 } else { 580 MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); 581 } 582 } 583 } 584 585 // An action batch consists of an ordered queue of Actions that can execute. 586 private final Queue<Action> mActions; 587 588 public ActionBatch() { 589 mActions = new LinkedList<>(); 590 } 591 592 public void add(final Action a) { 593 mActions.add(a); 594 } 595 596 /** 597 * Append all the actions of another action batch. 598 * @param that the upgrade to merge into this one. 599 */ 600 public void append(final ActionBatch that) { 601 for (final Action a : that.mActions) { 602 add(a); 603 } 604 } 605 606 /** 607 * Execute this batch. 608 * 609 * @param context the context for getting resources, databases, system services. 610 * @param reporter a Reporter to send errors to. 611 */ 612 public void execute(final Context context, final ProblemReporter reporter) { 613 DebugLogUtils.l("Executing a batch of actions"); 614 Queue<Action> remainingActions = mActions; 615 while (!remainingActions.isEmpty()) { 616 final Action a = remainingActions.poll(); 617 try { 618 a.execute(context); 619 } catch (Exception e) { 620 if (null != reporter) 621 reporter.report(e); 622 } 623 } 624 } 625} 626