1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.dictionarypack;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.sqlite.SQLiteDatabase;
23import android.database.sqlite.SQLiteOpenHelper;
24import android.text.TextUtils;
25import android.util.Log;
26
27import com.android.inputmethod.latin.R;
28import com.android.inputmethod.latin.utils.DebugLogUtils;
29
30import java.io.File;
31import java.util.ArrayList;
32import java.util.LinkedList;
33import java.util.List;
34import java.util.TreeMap;
35
36/**
37 * Various helper functions for the state database
38 */
39public 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