Home | History | Annotate | Download | only in dictionarypack
      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.latin.BinaryDictionaryFileDumper;
     29 import com.android.inputmethod.latin.R;
     30 import com.android.inputmethod.latin.common.LocaleUtils;
     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         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