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