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