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