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