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