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