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