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