MetadataDbHelper.java revision b9a16b88bf5b976e47b50f66b646f002954a5d83
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.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.sqlite.SQLiteDatabase;
23import android.database.sqlite.SQLiteException;
24import android.database.sqlite.SQLiteOpenHelper;
25import android.text.TextUtils;
26import android.util.Log;
27
28import com.android.inputmethod.latin.R;
29import com.android.inputmethod.latin.utils.DebugLogUtils;
30
31import java.io.File;
32import java.util.ArrayList;
33import java.util.LinkedList;
34import java.util.List;
35import java.util.TreeMap;
36
37/**
38 * Various helper functions for the state database
39 */
40public class MetadataDbHelper extends SQLiteOpenHelper {
41    private static final String TAG = MetadataDbHelper.class.getSimpleName();
42
43    // This was the initial release version of the database. It should never be
44    // changed going forward.
45    private static final int METADATA_DATABASE_INITIAL_VERSION = 3;
46    // This is the first released version of the database that implements CLIENTID. It is
47    // used to identify the versions for upgrades. This should never change going forward.
48    private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6;
49    // The current database version.
50    // This MUST be increased every time the dictionary pack metadata URL changes.
51    private static final int CURRENT_METADATA_DATABASE_VERSION = 13;
52
53    private final static long NOT_A_DOWNLOAD_ID = -1;
54
55    // The number of retries allowed when attempting to download a broken dictionary.
56    public static final int DICTIONARY_RETRY_THRESHOLD = 2;
57
58    public static final String METADATA_TABLE_NAME = "pendingUpdates";
59    static final String CLIENT_TABLE_NAME = "clients";
60    public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID
61    public static final String TYPE_COLUMN = "type";
62    public static final String STATUS_COLUMN = "status";
63    public static final String LOCALE_COLUMN = "locale";
64    public static final String WORDLISTID_COLUMN = "id";
65    public static final String DESCRIPTION_COLUMN = "description";
66    public static final String LOCAL_FILENAME_COLUMN = "filename";
67    public static final String REMOTE_FILENAME_COLUMN = "url";
68    public static final String DATE_COLUMN = "date";
69    public static final String CHECKSUM_COLUMN = "checksum";
70    public static final String FILESIZE_COLUMN = "filesize";
71    public static final String VERSION_COLUMN = "version";
72    public static final String FORMATVERSION_COLUMN = "formatversion";
73    public static final String FLAGS_COLUMN = "flags";
74    public static final String RAW_CHECKSUM_COLUMN = "rawChecksum";
75    public static final String RETRY_COUNT_COLUMN = "remainingRetries";
76    public static final int COLUMN_COUNT = 15;
77
78    private static final String CLIENT_CLIENT_ID_COLUMN = "clientid";
79    private static final String CLIENT_METADATA_URI_COLUMN = "uri";
80    private static final String CLIENT_METADATA_ADDITIONAL_ID_COLUMN = "additionalid";
81    private static final String CLIENT_LAST_UPDATE_DATE_COLUMN = "lastupdate";
82    private static final String CLIENT_PENDINGID_COLUMN = "pendingid"; // Download Manager ID
83
84    public static final String METADATA_DATABASE_NAME_STEM = "pendingUpdates";
85    public static final String METADATA_UPDATE_DESCRIPTION = "metadata";
86
87    public static final String DICTIONARIES_ASSETS_PATH = "dictionaries";
88
89    // Statuses, for storing in the STATUS_COLUMN
90    // IMPORTANT: The following are used as index arrays in ../WordListPreference
91    // Do not change their values without updating the matched code.
92    // Unknown status: this should never happen.
93    public static final int STATUS_UNKNOWN = 0;
94    // Available: this word list is available, but it is not downloaded (not downloading), because
95    // it is set not to be used.
96    public static final int STATUS_AVAILABLE = 1;
97    // Downloading: this word list is being downloaded.
98    public static final int STATUS_DOWNLOADING = 2;
99    // Installed: this word list is installed and usable.
100    public static final int STATUS_INSTALLED = 3;
101    // Disabled: this word list is installed, but has been disabled by the user.
102    public static final int STATUS_DISABLED = 4;
103    // Deleting: the user marked this word list to be deleted, but it has not been yet because
104    // Latin IME is not up yet.
105    public static final int STATUS_DELETING = 5;
106    // Retry: dictionary got corrupted, so an attempt must be done to download & install it again.
107    public static final int STATUS_RETRYING = 6;
108
109    // Types, for storing in the TYPE_COLUMN
110    // This is metadata about what is available.
111    public static final int TYPE_METADATA = 1;
112    // This is a bulk file. It should replace older files.
113    public static final int TYPE_BULK = 2;
114    // This is an incremental update, expected to be small, and meaningless on its own.
115    public static final int TYPE_UPDATE = 3;
116
117    private static final String METADATA_TABLE_CREATE =
118            "CREATE TABLE " + METADATA_TABLE_NAME + " ("
119            + PENDINGID_COLUMN + " INTEGER, "
120            + TYPE_COLUMN + " INTEGER, "
121            + STATUS_COLUMN + " INTEGER, "
122            + WORDLISTID_COLUMN + " TEXT, "
123            + LOCALE_COLUMN + " TEXT, "
124            + DESCRIPTION_COLUMN + " TEXT, "
125            + LOCAL_FILENAME_COLUMN + " TEXT, "
126            + REMOTE_FILENAME_COLUMN + " TEXT, "
127            + DATE_COLUMN + " INTEGER, "
128            + CHECKSUM_COLUMN + " TEXT, "
129            + FILESIZE_COLUMN + " INTEGER, "
130            + VERSION_COLUMN + " INTEGER,"
131            + FORMATVERSION_COLUMN + " INTEGER, "
132            + FLAGS_COLUMN + " INTEGER, "
133            + RAW_CHECKSUM_COLUMN + " TEXT,"
134            + RETRY_COUNT_COLUMN + " INTEGER, "
135            + "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));";
136    private static final String METADATA_CREATE_CLIENT_TABLE =
137            "CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " ("
138            + CLIENT_CLIENT_ID_COLUMN + " TEXT, "
139            + CLIENT_METADATA_URI_COLUMN + " TEXT, "
140            + CLIENT_METADATA_ADDITIONAL_ID_COLUMN + " TEXT, "
141            + CLIENT_LAST_UPDATE_DATE_COLUMN + " INTEGER NOT NULL DEFAULT 0, "
142            + CLIENT_PENDINGID_COLUMN + " INTEGER, "
143            + FLAGS_COLUMN + " INTEGER, "
144            + "PRIMARY KEY (" + CLIENT_CLIENT_ID_COLUMN + "));";
145
146    // List of all metadata table columns.
147    static final String[] METADATA_TABLE_COLUMNS = { PENDINGID_COLUMN, TYPE_COLUMN,
148            STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN,
149            LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN,
150            FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN,
151            RAW_CHECKSUM_COLUMN, RETRY_COUNT_COLUMN };
152    // List of all client table columns.
153    static final String[] CLIENT_TABLE_COLUMNS = { CLIENT_CLIENT_ID_COLUMN,
154            CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN };
155    // List of public columns returned to clients. Everything that is not in this list is
156    // private and implementation-dependent.
157    static final String[] DICTIONARIES_LIST_PUBLIC_COLUMNS = { STATUS_COLUMN, WORDLISTID_COLUMN,
158            LOCALE_COLUMN, DESCRIPTION_COLUMN, DATE_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN };
159
160    // This class exhibits a singleton-like behavior by client ID, so it is getInstance'd
161    // and has a private c'tor.
162    private static TreeMap<String, MetadataDbHelper> sInstanceMap = null;
163    public static synchronized MetadataDbHelper getInstance(final Context context,
164            final String clientIdOrNull) {
165        // As a backward compatibility feature, null can be passed here to retrieve the "default"
166        // database. Before multi-client support, the dictionary packed used only one database
167        // and would not be able to handle several dictionary sets. Passing null here retrieves
168        // this legacy database. New clients should make sure to always pass a client ID so as
169        // to avoid conflicts.
170        final String clientId = null != clientIdOrNull ? clientIdOrNull : "";
171        if (null == sInstanceMap) sInstanceMap = new TreeMap<>();
172        MetadataDbHelper helper = sInstanceMap.get(clientId);
173        if (null == helper) {
174            helper = new MetadataDbHelper(context, clientId);
175            sInstanceMap.put(clientId, helper);
176        }
177        return helper;
178    }
179    private MetadataDbHelper(final Context context, final String clientId) {
180        super(context,
181                METADATA_DATABASE_NAME_STEM + (TextUtils.isEmpty(clientId) ? "" : "." + clientId),
182                null, CURRENT_METADATA_DATABASE_VERSION);
183        mContext = context;
184        mClientId = clientId;
185    }
186
187    private final Context mContext;
188    private final String mClientId;
189
190    /**
191     * Get the database itself. This always returns the same object for any client ID. If the
192     * client ID is null, a default database is returned for backward compatibility. Don't
193     * pass null for new calls.
194     *
195     * @param context the context to create the database from. This is ignored after the first call.
196     * @param clientId the client id to retrieve the database of. null for default (deprecated)
197     * @return the database.
198     */
199    public static SQLiteDatabase getDb(final Context context, final String clientId) {
200        return getInstance(context, clientId).getWritableDatabase();
201    }
202
203    private void createClientTable(final SQLiteDatabase db) {
204        // The clients table only exists in the primary db, the one that has an empty client id
205        if (!TextUtils.isEmpty(mClientId)) return;
206        db.execSQL(METADATA_CREATE_CLIENT_TABLE);
207        final String defaultMetadataUri = mContext.getString(R.string.default_metadata_uri);
208        if (!TextUtils.isEmpty(defaultMetadataUri)) {
209            final ContentValues defaultMetadataValues = new ContentValues();
210            defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "");
211            defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri);
212            defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
213            db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues);
214        }
215    }
216
217    /**
218     * Create the table and populate it with the resources found inside the apk.
219     *
220     * @see SQLiteOpenHelper#onCreate(SQLiteDatabase)
221     *
222     * @param db the database to create and populate.
223     */
224    @Override
225    public void onCreate(final SQLiteDatabase db) {
226        db.execSQL(METADATA_TABLE_CREATE);
227        createClientTable(db);
228    }
229
230    private static void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db) {
231        try {
232            db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM "
233                    + METADATA_TABLE_NAME + " LIMIT 0;");
234        } catch (SQLiteException e) {
235            Log.i(TAG, "No " + RAW_CHECKSUM_COLUMN + " column : creating it");
236            db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN "
237                    + RAW_CHECKSUM_COLUMN + " TEXT;");
238        }
239    }
240
241    private static void addRetryCountColumnUnlessPresent(final SQLiteDatabase db) {
242        try {
243            db.execSQL("SELECT " + RETRY_COUNT_COLUMN + " FROM "
244                    + METADATA_TABLE_NAME + " LIMIT 0;");
245        } catch (SQLiteException e) {
246            Log.i(TAG, "No " + RETRY_COUNT_COLUMN + " column : creating it");
247            db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN "
248                    + RETRY_COUNT_COLUMN + " INTEGER DEFAULT " + DICTIONARY_RETRY_THRESHOLD + ";");
249        }
250    }
251
252    /**
253     * Upgrade the database. Upgrade from version 3 is supported.
254     * Version 3 has a DB named METADATA_DATABASE_NAME_STEM containing a table METADATA_TABLE_NAME.
255     * Version 6 and above has a DB named METADATA_DATABASE_NAME_STEM containing a
256     * table CLIENT_TABLE_NAME, and for each client a table called METADATA_TABLE_STEM + "." + the
257     * name of the client and contains a table METADATA_TABLE_NAME.
258     * For schemas, see the above create statements. The schemas have never changed so far.
259     *
260     * This method is called by the framework. See {@link SQLiteOpenHelper#onUpgrade}
261     * @param db The database we are upgrading
262     * @param oldVersion The old database version (the one on the disk)
263     * @param newVersion The new database version as supplied to the constructor of SQLiteOpenHelper
264     */
265    @Override
266    public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
267        if (METADATA_DATABASE_INITIAL_VERSION == oldVersion
268                && METADATA_DATABASE_VERSION_WITH_CLIENTID <= newVersion
269                && CURRENT_METADATA_DATABASE_VERSION >= newVersion) {
270            // Upgrade from version METADATA_DATABASE_INITIAL_VERSION to version
271            // METADATA_DATABASE_VERSION_WITH_CLIENT_ID
272            // Only the default database should contain the client table, so we test for mClientId.
273            if (TextUtils.isEmpty(mClientId)) {
274                // Anyway in version 3 only the default table existed so the emptiness
275                // test should always be true, but better check to be sure.
276                createClientTable(db);
277            }
278        } else if (METADATA_DATABASE_VERSION_WITH_CLIENTID < newVersion
279                && CURRENT_METADATA_DATABASE_VERSION >= newVersion) {
280            // Here we drop the client table, so that all clients send us their information again.
281            // The client table contains the URL to hit to update the available dictionaries list,
282            // but the info about the dictionaries themselves is stored in the table called
283            // METADATA_TABLE_NAME and we want to keep it, so we only drop the client table.
284            db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME);
285            // Only the default database should contain the client table, so we test for mClientId.
286            if (TextUtils.isEmpty(mClientId)) {
287                createClientTable(db);
288            }
289        } else {
290            // If we're not in the above case, either we are upgrading from an earlier versionCode
291            // and we should wipe the database, or we are handling a version we never heard about
292            // (can only be a bug) so it's safer to wipe the database.
293            db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME);
294            db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME);
295            onCreate(db);
296        }
297        // A rawChecksum column that did not exist in the previous versions was added that
298        // corresponds to the md5 checksum of the file after decompression/decryption. This is to
299        // strengthen the system against corrupted dictionary files.
300        // The most secure way to upgrade a database is to just test for the column presence, and
301        // add it if it's not there.
302        addRawChecksumColumnUnlessPresent(db);
303
304        // A retry count column that did not exist in the previous versions was added that
305        // corresponds to the number of download & installation attempts that have been made
306        // in order to strengthen the system recovery from corrupted dictionary files.
307        // The most secure way to upgrade a database is to just test for the column presence, and
308        // add it if it's not there.
309        addRetryCountColumnUnlessPresent(db);
310    }
311
312    /**
313     * Downgrade the database. This drops and recreates the table in all cases.
314     */
315    @Override
316    public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
317        // No matter what the numerical values of oldVersion and newVersion are, we know this
318        // is a downgrade (newVersion < oldVersion). There is no way to know what the future
319        // databases will look like, but we know it's extremely likely that it's okay to just
320        // drop the tables and start from scratch. Hence, we ignore the versions and just wipe
321        // everything we want to use.
322        if (oldVersion <= newVersion) {
323            Log.e(TAG, "onDowngrade database but new version is higher? " + oldVersion + " <= "
324                    + newVersion);
325        }
326        db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME);
327        db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME);
328        onCreate(db);
329    }
330
331    /**
332     * Given a client ID, returns whether this client exists.
333     *
334     * @param context a context to open the database
335     * @param clientId the client ID to check
336     * @return true if the client is known, false otherwise
337     */
338    public static boolean isClientKnown(final Context context, final String clientId) {
339        // If the client is known, they'll have a non-null metadata URI. An empty string is
340        // allowed as a metadata URI, if the client doesn't want any updates to happen.
341        return null != getMetadataUriAsString(context, clientId);
342    }
343
344    /**
345     * Returns the metadata URI as a string.
346     *
347     * If the client is not known, this will return null. If it is known, it will return
348     * the URI as a string. Note that the empty string is a valid value.
349     *
350     * @param context a context instance to open the database on
351     * @param clientId the ID of the client we want the metadata URI of
352     * @return the string representation of the URI
353     */
354    public static String getMetadataUriAsString(final Context context, final String clientId) {
355        SQLiteDatabase defaultDb = MetadataDbHelper.getDb(context, null);
356        final Cursor cursor = defaultDb.query(MetadataDbHelper.CLIENT_TABLE_NAME,
357                new String[] { MetadataDbHelper.CLIENT_METADATA_URI_COLUMN,
358                        MetadataDbHelper.CLIENT_METADATA_ADDITIONAL_ID_COLUMN },
359                MetadataDbHelper.CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId },
360                null, null, null, null);
361        try {
362            if (!cursor.moveToFirst()) return null;
363            return MetadataUriGetter.getUri(context, cursor.getString(0), cursor.getString(1));
364        } finally {
365            cursor.close();
366        }
367    }
368
369    /**
370     * Update the last metadata update time for all clients using a particular URI.
371     *
372     * This method searches for all clients using a particular URI and updates the last
373     * update time for this client.
374     * The current time is used as the latest update time. This saved date will be what
375     * is returned henceforth by {@link #getLastUpdateDateForClient(Context, String)},
376     * until this method is called again.
377     *
378     * @param context a context instance to open the database on
379     * @param uri the metadata URI we just downloaded
380     */
381    public static void saveLastUpdateTimeOfUri(final Context context, final String uri) {
382        PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis());
383        final ContentValues values = new ContentValues();
384        values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis());
385        final SQLiteDatabase defaultDb = getDb(context, null);
386        final Cursor cursor = MetadataDbHelper.queryClientIds(context);
387        if (null == cursor) return;
388        try {
389            if (!cursor.moveToFirst()) return;
390            do {
391                final String clientId = cursor.getString(0);
392                final String metadataUri =
393                        MetadataDbHelper.getMetadataUriAsString(context, clientId);
394                if (metadataUri.equals(uri)) {
395                    defaultDb.update(CLIENT_TABLE_NAME, values,
396                            CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
397                }
398            } while (cursor.moveToNext());
399        } finally {
400            cursor.close();
401        }
402    }
403
404    /**
405     * Retrieves the last date at which we updated the metadata for this client.
406     *
407     * The returned date is in milliseconds from the EPOCH; this is the same unit as
408     * returned by {@link System#currentTimeMillis()}.
409     *
410     * @param context a context instance to open the database on
411     * @param clientId the client ID to get the latest update date of
412     * @return the last date at which this client was updated, as a long.
413     */
414    public static long getLastUpdateDateForClient(final Context context, final String clientId) {
415        SQLiteDatabase defaultDb = getDb(context, null);
416        final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
417                new String[] { CLIENT_LAST_UPDATE_DATE_COLUMN },
418                CLIENT_CLIENT_ID_COLUMN + " = ?",
419                new String[] { null == clientId ? "" : clientId },
420                null, null, null, null);
421        try {
422            if (!cursor.moveToFirst()) return 0;
423            return cursor.getLong(0); // Only one column, return it
424        } finally {
425            cursor.close();
426        }
427    }
428
429    /**
430     * Get the metadata download ID for a metadata URI.
431     *
432     * This will retrieve the download ID for the metadata file that has the passed URI.
433     * If this URI is not being downloaded right now, it will return NOT_AN_ID.
434     *
435     * @param context a context instance to open the database on
436     * @param uri the URI to retrieve the metadata download ID of
437     * @return the download id and start date, or null if the URL is not known
438     */
439    public static DownloadIdAndStartDate getMetadataDownloadIdAndStartDateForURI(
440            final Context context, final String uri) {
441        SQLiteDatabase defaultDb = getDb(context, null);
442        final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
443                new String[] { CLIENT_PENDINGID_COLUMN, CLIENT_LAST_UPDATE_DATE_COLUMN },
444                CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri },
445                null, null, null, null);
446        try {
447            if (!cursor.moveToFirst()) return null;
448            return new DownloadIdAndStartDate(cursor.getInt(0), cursor.getLong(1));
449        } finally {
450            cursor.close();
451        }
452    }
453
454    public static long getOldestUpdateTime(final Context context) {
455        SQLiteDatabase defaultDb = getDb(context, null);
456        final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
457                new String[] { CLIENT_LAST_UPDATE_DATE_COLUMN },
458                null, null, null, null, null);
459        try {
460            if (!cursor.moveToFirst()) return 0;
461            final int columnIndex = 0; // Only one column queried
462            // Initialize the earliestTime to the largest possible value.
463            long earliestTime = Long.MAX_VALUE; // Almost 300 million years in the future
464            do {
465                final long thisTime = cursor.getLong(columnIndex);
466                earliestTime = Math.min(thisTime, earliestTime);
467            } while (cursor.moveToNext());
468            return earliestTime;
469        } finally {
470            cursor.close();
471        }
472    }
473
474    /**
475     * Helper method to make content values to write into the database.
476     * @return content values with all the arguments put with the right column names.
477     */
478    public static ContentValues makeContentValues(final int pendingId, final int type,
479            final int status, final String wordlistId, final String locale,
480            final String description, final String filename, final String url, final long date,
481            final String rawChecksum, final String checksum, final int retryCount,
482            final long filesize, final int version, final int formatVersion) {
483        final ContentValues result = new ContentValues(COLUMN_COUNT);
484        result.put(PENDINGID_COLUMN, pendingId);
485        result.put(TYPE_COLUMN, type);
486        result.put(WORDLISTID_COLUMN, wordlistId);
487        result.put(STATUS_COLUMN, status);
488        result.put(LOCALE_COLUMN, locale);
489        result.put(DESCRIPTION_COLUMN, description);
490        result.put(LOCAL_FILENAME_COLUMN, filename);
491        result.put(REMOTE_FILENAME_COLUMN, url);
492        result.put(DATE_COLUMN, date);
493        result.put(RAW_CHECKSUM_COLUMN, rawChecksum);
494        result.put(RETRY_COUNT_COLUMN, retryCount);
495        result.put(CHECKSUM_COLUMN, checksum);
496        result.put(FILESIZE_COLUMN, filesize);
497        result.put(VERSION_COLUMN, version);
498        result.put(FORMATVERSION_COLUMN, formatVersion);
499        result.put(FLAGS_COLUMN, 0);
500        return result;
501    }
502
503    /**
504     * Helper method to fill in an incomplete ContentValues with default values.
505     * A wordlist ID and a locale are required, otherwise BadFormatException is thrown.
506     * @return the same object that was passed in, completed with default values.
507     */
508    public static ContentValues completeWithDefaultValues(final ContentValues result)
509            throws BadFormatException {
510        if (null == result.get(WORDLISTID_COLUMN) || null == result.get(LOCALE_COLUMN)) {
511            throw new BadFormatException();
512        }
513        // 0 for the pending id, because there is none
514        if (null == result.get(PENDINGID_COLUMN)) result.put(PENDINGID_COLUMN, 0);
515        // This is a binary blob of a dictionary
516        if (null == result.get(TYPE_COLUMN)) result.put(TYPE_COLUMN, TYPE_BULK);
517        // This word list is unknown, but it's present, else we wouldn't be here, so INSTALLED
518        if (null == result.get(STATUS_COLUMN)) result.put(STATUS_COLUMN, STATUS_INSTALLED);
519        // No description unless specified, because we can't guess it
520        if (null == result.get(DESCRIPTION_COLUMN)) result.put(DESCRIPTION_COLUMN, "");
521        // File name - this is an asset, so it works as an already deleted file.
522        //     hence, we need to supply a non-existent file name. Anything will
523        //     do as long as it returns false when tested with File#exist(), and
524        //     the empty string does not, so it's set to "_".
525        if (null == result.get(LOCAL_FILENAME_COLUMN)) result.put(LOCAL_FILENAME_COLUMN, "_");
526        // No remote file name : this can't be downloaded. Unless specified.
527        if (null == result.get(REMOTE_FILENAME_COLUMN)) result.put(REMOTE_FILENAME_COLUMN, "");
528        // 0 for the update date : 1970/1/1. Unless specified.
529        if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0);
530        // Raw checksum unknown unless specified
531        if (null == result.get(RAW_CHECKSUM_COLUMN)) result.put(RAW_CHECKSUM_COLUMN, "");
532        // Retry column 0 unless specified
533        if (null == result.get(RETRY_COUNT_COLUMN)) result.put(RETRY_COUNT_COLUMN,
534                DICTIONARY_RETRY_THRESHOLD);
535        // Checksum unknown unless specified
536        if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, "");
537        // No filesize unless specified
538        if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0);
539        // Smallest possible version unless specified
540        if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1);
541        // Assume current format unless specified
542        if (null == result.get(FORMATVERSION_COLUMN))
543            result.put(FORMATVERSION_COLUMN, UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION);
544        // No flags unless specified
545        if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0);
546        return result;
547    }
548
549    /**
550     * Reads a column in a Cursor as a String and stores it in a ContentValues object.
551     * @param result the ContentValues object to store the result in.
552     * @param cursor the Cursor to read the column from.
553     * @param columnId the column ID to read.
554     */
555    private static void putStringResult(ContentValues result, Cursor cursor, String columnId) {
556        result.put(columnId, cursor.getString(cursor.getColumnIndex(columnId)));
557    }
558
559    /**
560     * Reads a column in a Cursor as an int and stores it in a ContentValues object.
561     * @param result the ContentValues object to store the result in.
562     * @param cursor the Cursor to read the column from.
563     * @param columnId the column ID to read.
564     */
565    private static void putIntResult(ContentValues result, Cursor cursor, String columnId) {
566        result.put(columnId, cursor.getInt(cursor.getColumnIndex(columnId)));
567    }
568
569    private static ContentValues getFirstLineAsContentValues(final Cursor cursor) {
570        final ContentValues result;
571        if (cursor.moveToFirst()) {
572            result = new ContentValues(COLUMN_COUNT);
573            putIntResult(result, cursor, PENDINGID_COLUMN);
574            putIntResult(result, cursor, TYPE_COLUMN);
575            putIntResult(result, cursor, STATUS_COLUMN);
576            putStringResult(result, cursor, WORDLISTID_COLUMN);
577            putStringResult(result, cursor, LOCALE_COLUMN);
578            putStringResult(result, cursor, DESCRIPTION_COLUMN);
579            putStringResult(result, cursor, LOCAL_FILENAME_COLUMN);
580            putStringResult(result, cursor, REMOTE_FILENAME_COLUMN);
581            putIntResult(result, cursor, DATE_COLUMN);
582            putStringResult(result, cursor, RAW_CHECKSUM_COLUMN);
583            putStringResult(result, cursor, CHECKSUM_COLUMN);
584            putIntResult(result, cursor, RETRY_COUNT_COLUMN);
585            putIntResult(result, cursor, FILESIZE_COLUMN);
586            putIntResult(result, cursor, VERSION_COLUMN);
587            putIntResult(result, cursor, FORMATVERSION_COLUMN);
588            putIntResult(result, cursor, FLAGS_COLUMN);
589            if (cursor.moveToNext()) {
590                // TODO: print the second level of the stack to the log so that we know
591                // in which code path the error happened
592                Log.e(TAG, "Several SQL results when we expected only one!");
593            }
594        } else {
595            result = null;
596        }
597        return result;
598    }
599
600    /**
601     * Gets the info about as specific download, indexed by its DownloadManager ID.
602     * @param db the database to get the information from.
603     * @param id the DownloadManager id.
604     * @return metadata about this download. This returns all columns in the database.
605     */
606    public static ContentValues getContentValuesByPendingId(final SQLiteDatabase db,
607            final long id) {
608        final Cursor cursor = db.query(METADATA_TABLE_NAME,
609                METADATA_TABLE_COLUMNS,
610                PENDINGID_COLUMN + "= ?",
611                new String[] { Long.toString(id) },
612                null, null, null);
613        if (null == cursor) {
614            return null;
615        }
616        try {
617            // There should never be more than one result. If because of some bug there are,
618            // returning only one result is the right thing to do, because we couldn't handle
619            // several anyway and we should still handle one.
620            return getFirstLineAsContentValues(cursor);
621        } finally {
622            cursor.close();
623        }
624    }
625
626    /**
627     * Gets the info about an installed OR deleting word list with a specified id.
628     *
629     * Basically, this is the word list that we want to return to Android Keyboard when
630     * it asks for a specific id.
631     *
632     * @param db the database to get the information from.
633     * @param id the word list ID.
634     * @return the metadata about this word list.
635     */
636    public static ContentValues getInstalledOrDeletingWordListContentValuesByWordListId(
637            final SQLiteDatabase db, final String id) {
638        final Cursor cursor = db.query(METADATA_TABLE_NAME,
639                METADATA_TABLE_COLUMNS,
640                WORDLISTID_COLUMN + "=? AND (" + STATUS_COLUMN + "=? OR " + STATUS_COLUMN + "=?)",
641                new String[] { id, Integer.toString(STATUS_INSTALLED),
642                        Integer.toString(STATUS_DELETING) },
643                null, null, null);
644        if (null == cursor) {
645            return null;
646        }
647        try {
648            // There should only be one result, but if there are several, we can't tell which
649            // is the best, so we just return the first one.
650            return getFirstLineAsContentValues(cursor);
651        } finally {
652            cursor.close();
653        }
654    }
655
656    /**
657     * Given a specific download ID, return records for all pending downloads across all clients.
658     *
659     * If several clients use the same metadata URL, we know to only download it once, and
660     * dispatch the update process across all relevant clients when the download ends. This means
661     * several clients may share a single download ID if they share a metadata URI.
662     * The dispatching is done in
663     * {@link UpdateHandler#downloadFinished(Context, android.content.Intent)}, which
664     * finds out about the list of relevant clients by calling this method.
665     *
666     * @param context a context instance to open the databases
667     * @param downloadId the download ID to query about
668     * @return the list of records. Never null, but may be empty.
669     */
670    public static ArrayList<DownloadRecord> getDownloadRecordsForDownloadId(final Context context,
671            final long downloadId) {
672        final SQLiteDatabase defaultDb = getDb(context, "");
673        final ArrayList<DownloadRecord> results = new ArrayList<>();
674        final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, CLIENT_TABLE_COLUMNS,
675                null, null, null, null, null);
676        try {
677            if (!cursor.moveToFirst()) return results;
678            final int clientIdIndex = cursor.getColumnIndex(CLIENT_CLIENT_ID_COLUMN);
679            final int pendingIdColumn = cursor.getColumnIndex(CLIENT_PENDINGID_COLUMN);
680            do {
681                final long pendingId = cursor.getInt(pendingIdColumn);
682                final String clientId = cursor.getString(clientIdIndex);
683                if (pendingId == downloadId) {
684                    results.add(new DownloadRecord(clientId, null));
685                }
686                final ContentValues valuesForThisClient =
687                        getContentValuesByPendingId(getDb(context, clientId), downloadId);
688                if (null != valuesForThisClient) {
689                    results.add(new DownloadRecord(clientId, valuesForThisClient));
690                }
691            } while (cursor.moveToNext());
692        } finally {
693            cursor.close();
694        }
695        return results;
696    }
697
698    /**
699     * Gets the info about a specific word list.
700     *
701     * @param db the database to get the information from.
702     * @param id the word list ID.
703     * @param version the word list version.
704     * @return the metadata about this word list.
705     */
706    public static ContentValues getContentValuesByWordListId(final SQLiteDatabase db,
707            final String id, final int version) {
708        final Cursor cursor = db.query(METADATA_TABLE_NAME,
709                METADATA_TABLE_COLUMNS,
710                WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ? AND "
711                        + FORMATVERSION_COLUMN + "<= ?",
712                new String[]
713                        { id,
714                          Integer.toString(version),
715                          Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION)
716                        },
717                null /* groupBy */,
718                null /* having */,
719                FORMATVERSION_COLUMN + " DESC"/* orderBy */);
720        if (null == cursor) {
721            return null;
722        }
723        try {
724            // This is a lookup by primary key, so there can't be more than one result.
725            return getFirstLineAsContentValues(cursor);
726        } finally {
727            cursor.close();
728        }
729    }
730
731    /**
732     * Gets the info about the latest word list with an id.
733     *
734     * @param db the database to get the information from.
735     * @param id the word list ID.
736     * @return the metadata about the word list with this id and the latest version number.
737     */
738    public static ContentValues getContentValuesOfLatestAvailableWordlistById(
739            final SQLiteDatabase db, final String id) {
740        final Cursor cursor = db.query(METADATA_TABLE_NAME,
741                METADATA_TABLE_COLUMNS,
742                WORDLISTID_COLUMN + "= ?",
743                new String[] { id }, null, null, VERSION_COLUMN + " DESC", "1");
744        if (null == cursor) {
745            return null;
746        }
747        try {
748            // Return the first result from the list of results.
749            return getFirstLineAsContentValues(cursor);
750        } finally {
751            cursor.close();
752        }
753    }
754
755    /**
756     * Gets the current metadata about INSTALLED, AVAILABLE or DELETING dictionaries.
757     *
758     * This odd method is tailored to the needs of
759     * DictionaryProvider#getDictionaryWordListsForContentUri, which needs the word list if
760     * it is:
761     * - INSTALLED: this should be returned to LatinIME if the file is still inside the dictionary
762     * pack, so that it can be copied. If the file is not there, it's been copied already and should
763     * not be returned, so getDictionaryWordListsForContentUri takes care of this.
764     * - DELETING: this should be returned to LatinIME so that it can actually delete the file.
765     * - AVAILABLE: this should not be returned, but should be checked for auto-installation.
766     *
767     * @param context the context for getting the database.
768     * @param clientId the client id for retrieving the database. null for default (deprecated)
769     * @return a cursor with metadata about usable dictionaries.
770     */
771    public static Cursor queryInstalledOrDeletingOrAvailableDictionaryMetadata(
772            final Context context, final String clientId) {
773        // If clientId is null, we get the defaut DB (see #getInstance() for more about this)
774        final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME,
775                METADATA_TABLE_COLUMNS,
776                STATUS_COLUMN + " = ? OR " + STATUS_COLUMN + " = ? OR " + STATUS_COLUMN + " = ?",
777                new String[] { Integer.toString(STATUS_INSTALLED),
778                        Integer.toString(STATUS_DELETING),
779                        Integer.toString(STATUS_AVAILABLE) },
780                null, null, LOCALE_COLUMN);
781        return results;
782    }
783
784    /**
785     * Gets the current metadata about all dictionaries.
786     *
787     * This will retrieve the metadata about all dictionaries, including
788     * older files, or files not yet downloaded.
789     *
790     * @param context the context for getting the database.
791     * @param clientId the client id for retrieving the database. null for default (deprecated)
792     * @return a cursor with metadata about usable dictionaries.
793     */
794    public static Cursor queryCurrentMetadata(final Context context, final String clientId) {
795        // If clientId is null, we get the defaut DB (see #getInstance() for more about this)
796        final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME,
797                METADATA_TABLE_COLUMNS, null, null, null, null, LOCALE_COLUMN);
798        return results;
799    }
800
801    /**
802     * Gets the list of all dictionaries known to the dictionary provider, with only public columns.
803     *
804     * This will retrieve information about all known dictionaries, and their status. As such,
805     * it will also return information about dictionaries on the server that have not been
806     * downloaded yet, but may be requested.
807     * This only returns public columns. It does not populate internal columns in the returned
808     * cursor.
809     * The value returned by this method is intended to be good to be returned directly for a
810     * request of the list of dictionaries by a client.
811     *
812     * @param context the context to read the database from.
813     * @param clientId the client id for retrieving the database. null for default (deprecated)
814     * @return a cursor that lists all available dictionaries and their metadata.
815     */
816    public static Cursor queryDictionaries(final Context context, final String clientId) {
817        // If clientId is null, we get the defaut DB (see #getInstance() for more about this)
818        final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME,
819                DICTIONARIES_LIST_PUBLIC_COLUMNS,
820                // Filter out empty locales so as not to return auxiliary data, like a
821                // data line for downloading metadata:
822                MetadataDbHelper.LOCALE_COLUMN + " != ?", new String[] {""},
823                // TODO: Reinstate the following code for bulk, then implement partial updates
824                /*                MetadataDbHelper.TYPE_COLUMN + " = ?",
825                new String[] { Integer.toString(MetadataDbHelper.TYPE_BULK) }, */
826                null, null, LOCALE_COLUMN);
827        return results;
828    }
829
830    /**
831     * Deletes all data associated with a client.
832     *
833     * @param context the context for opening the database
834     * @param clientId the ID of the client to delete.
835     * @return true if the client was successfully deleted, false otherwise.
836     */
837    public static boolean deleteClient(final Context context, final String clientId) {
838        // Remove all metadata associated with this client
839        final SQLiteDatabase db = getDb(context, clientId);
840        db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME);
841        db.execSQL(METADATA_TABLE_CREATE);
842        // Remove this client's entry in the clients table
843        final SQLiteDatabase defaultDb = getDb(context, "");
844        if (0 == defaultDb.delete(CLIENT_TABLE_NAME,
845                CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId })) {
846            return false;
847        }
848        return true;
849    }
850
851    /**
852     * Updates information relative to a specific client.
853     *
854     * Updatable information includes the metadata URI and the additional ID column. It may be
855     * expanded in the future.
856     * The passed values must include a client ID in the key CLIENT_CLIENT_ID_COLUMN, and it must
857     * be equal to the string passed as an argument for clientId. It may not be empty.
858     * The passed values must also include a non-null metadata URI in the
859     * CLIENT_METADATA_URI_COLUMN column, as well as a non-null additional ID in the
860     * CLIENT_METADATA_ADDITIONAL_ID_COLUMN. Both these strings may be empty.
861     * If any of the above is not complied with, this function returns without updating data.
862     *
863     * @param context the context, to open the database
864     * @param clientId the ID of the client to update
865     * @param values the values to update. Must conform to the protocol (see above)
866     */
867    public static void updateClientInfo(final Context context, final String clientId,
868            final ContentValues values) {
869        // Sanity check the content values
870        final String valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN);
871        final String valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN);
872        final String valuesMetadataAdditionalId =
873                values.getAsString(CLIENT_METADATA_ADDITIONAL_ID_COLUMN);
874        // Empty string is a valid client ID, but external apps may not configure it, so disallow
875        // both null and empty string.
876        // Empty string is a valid metadata URI if the client does not want updates, so allow
877        // empty string but disallow null.
878        // Empty string is a valid additional ID so allow empty string but disallow null.
879        if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri
880                || null == valuesMetadataAdditionalId) {
881            // We need all these columns to be filled in
882            DebugLogUtils.l("Missing parameter for updateClientInfo");
883            return;
884        }
885        if (!clientId.equals(valuesClientId)) {
886            // Mismatch! The client violates the protocol.
887            DebugLogUtils.l("Received an updateClientInfo request for ", clientId,
888                    " but the values " + "contain a different ID : ", valuesClientId);
889            return;
890        }
891        // Default value for a pending ID is NOT_AN_ID
892        values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
893        final SQLiteDatabase defaultDb = getDb(context, "");
894        if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
895            defaultDb.update(CLIENT_TABLE_NAME, values,
896                    CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
897        }
898    }
899
900    /**
901     * Retrieves the list of existing client IDs.
902     * @param context the context to open the database
903     * @return a cursor containing only one column, and one client ID per line.
904     */
905    public static Cursor queryClientIds(final Context context) {
906        return getDb(context, null).query(CLIENT_TABLE_NAME,
907                new String[] { CLIENT_CLIENT_ID_COLUMN }, null, null, null, null, null);
908    }
909
910    /**
911     * Register a download ID for a specific metadata URI.
912     *
913     * This method should be called when a download for a metadata URI is starting. It will
914     * search for all clients using this metadata URI and will register for each of them
915     * the download ID into the database for later retrieval by
916     * {@link #getDownloadRecordsForDownloadId(Context, long)}.
917     *
918     * @param context a context for opening databases
919     * @param uri the metadata URI
920     * @param downloadId the download ID
921     */
922    public static void registerMetadataDownloadId(final Context context, final String uri,
923            final long downloadId) {
924        final ContentValues values = new ContentValues();
925        values.put(CLIENT_PENDINGID_COLUMN, downloadId);
926        values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis());
927        final SQLiteDatabase defaultDb = getDb(context, "");
928        final Cursor cursor = MetadataDbHelper.queryClientIds(context);
929        if (null == cursor) return;
930        try {
931            if (!cursor.moveToFirst()) return;
932            do {
933                final String clientId = cursor.getString(0);
934                final String metadataUri =
935                        MetadataDbHelper.getMetadataUriAsString(context, clientId);
936                if (metadataUri.equals(uri)) {
937                    defaultDb.update(CLIENT_TABLE_NAME, values,
938                            CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
939                }
940            } while (cursor.moveToNext());
941        } finally {
942            cursor.close();
943        }
944    }
945
946    /**
947     * Marks a downloading entry as having successfully downloaded and being installed.
948     *
949     * The metadata database contains information about ongoing processes, typically ongoing
950     * downloads. This marks such an entry as having finished and having installed successfully,
951     * so it becomes INSTALLED.
952     *
953     * @param db the metadata database.
954     * @param r content values about the entry to mark as processed.
955     */
956    public static void markEntryAsFinishedDownloadingAndInstalled(final SQLiteDatabase db,
957            final ContentValues r) {
958        switch (r.getAsInteger(TYPE_COLUMN)) {
959            case TYPE_BULK:
960                DebugLogUtils.l("Ended processing a wordlist");
961                // Updating a bulk word list is a three-step operation:
962                // - Add the new entry to the table
963                // - Remove the old entry from the table
964                // - Erase the old file
965                // We start by gathering the names of the files we should delete.
966                final List<String> filenames = new LinkedList<>();
967                final Cursor c = db.query(METADATA_TABLE_NAME,
968                        new String[] { LOCAL_FILENAME_COLUMN },
969                        LOCALE_COLUMN + " = ? AND " +
970                        WORDLISTID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?",
971                        new String[] { r.getAsString(LOCALE_COLUMN),
972                                r.getAsString(WORDLISTID_COLUMN),
973                                Integer.toString(STATUS_INSTALLED) },
974                        null, null, null);
975                try {
976                    if (c.moveToFirst()) {
977                        // There should never be more than one file, but if there are, it's a bug
978                        // and we should remove them all. I think it might happen if the power of
979                        // the phone is suddenly cut during an update.
980                        final int filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN);
981                        do {
982                            DebugLogUtils.l("Setting for removal", c.getString(filenameIndex));
983                            filenames.add(c.getString(filenameIndex));
984                        } while (c.moveToNext());
985                    }
986                } finally {
987                    c.close();
988                }
989                r.put(STATUS_COLUMN, STATUS_INSTALLED);
990                db.beginTransactionNonExclusive();
991                // Delete all old entries. There should never be any stalled entries, but if
992                // there are, this deletes them.
993                db.delete(METADATA_TABLE_NAME,
994                        WORDLISTID_COLUMN + " = ?",
995                        new String[] { r.getAsString(WORDLISTID_COLUMN) });
996                db.insert(METADATA_TABLE_NAME, null, r);
997                db.setTransactionSuccessful();
998                db.endTransaction();
999                for (String filename : filenames) {
1000                    try {
1001                        final File f = new File(filename);
1002                        f.delete();
1003                    } catch (SecurityException e) {
1004                        // No permissions to delete. Um. Can't do anything.
1005                    } // I don't think anything else can be thrown
1006                }
1007                break;
1008            default:
1009                // Unknown type: do nothing.
1010                break;
1011        }
1012     }
1013
1014    /**
1015     * Removes a downloading entry from the database.
1016     *
1017     * This is invoked when a download fails. Either we tried to download, but
1018     * we received a permanent failure and we should remove it, or we got manually
1019     * cancelled and we should leave it at that.
1020     *
1021     * @param db the metadata database.
1022     * @param id the DownloadManager id of the file.
1023     */
1024    public static void deleteDownloadingEntry(final SQLiteDatabase db, final long id) {
1025        db.delete(METADATA_TABLE_NAME, PENDINGID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?",
1026                new String[] { Long.toString(id), Integer.toString(STATUS_DOWNLOADING) });
1027    }
1028
1029    /**
1030     * Forcefully removes an entry from the database.
1031     *
1032     * This is invoked when a file is broken. The file has been downloaded, but Android
1033     * Keyboard is telling us it could not open it.
1034     *
1035     * @param db the metadata database.
1036     * @param id the id of the word list.
1037     * @param version the version of the word list.
1038     */
1039    public static void deleteEntry(final SQLiteDatabase db, final String id, final int version) {
1040        db.delete(METADATA_TABLE_NAME, WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?",
1041                new String[] { id, Integer.toString(version) });
1042    }
1043
1044    /**
1045     * Internal method that sets the current status of an entry of the database.
1046     *
1047     * @param db the metadata database.
1048     * @param id the id of the word list.
1049     * @param version the version of the word list.
1050     * @param status the status to set the word list to.
1051     * @param downloadId an optional download id to write, or NOT_A_DOWNLOAD_ID
1052     */
1053    private static void markEntryAs(final SQLiteDatabase db, final String id,
1054            final int version, final int status, final long downloadId) {
1055        final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version);
1056        values.put(STATUS_COLUMN, status);
1057        if (NOT_A_DOWNLOAD_ID != downloadId) {
1058            values.put(MetadataDbHelper.PENDINGID_COLUMN, downloadId);
1059        }
1060        db.update(METADATA_TABLE_NAME, values,
1061                WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?",
1062                new String[] { id, Integer.toString(version) });
1063    }
1064
1065    /**
1066     * Writes the status column for the wordlist with this id as enabled. Typically this
1067     * means the word list is currently disabled and we want to set its status to INSTALLED.
1068     *
1069     * @param db the metadata database.
1070     * @param id the id of the word list.
1071     * @param version the version of the word list.
1072     */
1073    public static void markEntryAsEnabled(final SQLiteDatabase db, final String id,
1074            final int version) {
1075        markEntryAs(db, id, version, STATUS_INSTALLED, NOT_A_DOWNLOAD_ID);
1076    }
1077
1078    /**
1079     * Writes the status column for the wordlist with this id as disabled. Typically this
1080     * means the word list is currently installed and we want to set its status to DISABLED.
1081     *
1082     * @param db the metadata database.
1083     * @param id the id of the word list.
1084     * @param version the version of the word list.
1085     */
1086    public static void markEntryAsDisabled(final SQLiteDatabase db, final String id,
1087            final int version) {
1088        markEntryAs(db, id, version, STATUS_DISABLED, NOT_A_DOWNLOAD_ID);
1089    }
1090
1091    /**
1092     * Writes the status column for the wordlist with this id as available. This happens for
1093     * example when a word list has been deleted but can be downloaded again.
1094     *
1095     * @param db the metadata database.
1096     * @param id the id of the word list.
1097     * @param version the version of the word list.
1098     */
1099    public static void markEntryAsAvailable(final SQLiteDatabase db, final String id,
1100            final int version) {
1101        markEntryAs(db, id, version, STATUS_AVAILABLE, NOT_A_DOWNLOAD_ID);
1102    }
1103
1104    /**
1105     * Writes the designated word list as downloadable, alongside with its download id.
1106     *
1107     * @param db the metadata database.
1108     * @param id the id of the word list.
1109     * @param version the version of the word list.
1110     * @param downloadId the download id.
1111     */
1112    public static void markEntryAsDownloading(final SQLiteDatabase db, final String id,
1113            final int version, final long downloadId) {
1114        markEntryAs(db, id, version, STATUS_DOWNLOADING, downloadId);
1115    }
1116
1117    /**
1118     * Writes the designated word list as deleting.
1119     *
1120     * @param db the metadata database.
1121     * @param id the id of the word list.
1122     * @param version the version of the word list.
1123     */
1124    public static void markEntryAsDeleting(final SQLiteDatabase db, final String id,
1125            final int version) {
1126        markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID);
1127    }
1128
1129    /**
1130     * Checks retry counts and marks the word list as retrying if retry is possible.
1131     *
1132     * @param db the metadata database.
1133     * @param id the id of the word list.
1134     * @param version the version of the word list.
1135     * @return {@code true} if the retry is possible.
1136     */
1137    public static boolean maybeMarkEntryAsRetrying(final SQLiteDatabase db, final String id,
1138            final int version) {
1139        final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version);
1140        int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN);
1141        if (retryCount > 1) {
1142            values.put(STATUS_COLUMN, STATUS_RETRYING);
1143            values.put(RETRY_COUNT_COLUMN, retryCount - 1);
1144            db.update(METADATA_TABLE_NAME, values,
1145                    WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?",
1146                    new String[] { id, Integer.toString(version) });
1147            return true;
1148        }
1149        return false;
1150    }
1151}
1152