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