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