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