1156 lines
55 KiB
Java
1156 lines
55 KiB
Java
/*
|
|
* Copyright (C) 2011 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
* use this file except in compliance with the License. You may obtain a copy of
|
|
* the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations under
|
|
* the License.
|
|
*/
|
|
|
|
package com.android.inputmethod.dictionarypack;
|
|
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteException;
|
|
import android.database.sqlite.SQLiteOpenHelper;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import com.android.inputmethod.latin.R;
|
|
import com.android.inputmethod.latin.utils.DebugLogUtils;
|
|
|
|
import java.io.File;
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.TreeMap;
|
|
|
|
import javax.annotation.Nullable;
|
|
|
|
/**
|
|
* Various helper functions for the state database
|
|
*/
|
|
public class MetadataDbHelper extends SQLiteOpenHelper {
|
|
private static final String TAG = MetadataDbHelper.class.getSimpleName();
|
|
|
|
// This was the initial release version of the database. It should never be
|
|
// changed going forward.
|
|
private static final int METADATA_DATABASE_INITIAL_VERSION = 3;
|
|
// This is the first released version of the database that implements CLIENTID. It is
|
|
// used to identify the versions for upgrades. This should never change going forward.
|
|
private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6;
|
|
// The current database version.
|
|
// This MUST be increased every time the dictionary pack metadata URL changes.
|
|
private static final int CURRENT_METADATA_DATABASE_VERSION = 16;
|
|
|
|
private final static long NOT_A_DOWNLOAD_ID = -1;
|
|
|
|
// The number of retries allowed when attempting to download a broken dictionary.
|
|
public static final int DICTIONARY_RETRY_THRESHOLD = 2;
|
|
|
|
public static final String METADATA_TABLE_NAME = "pendingUpdates";
|
|
static final String CLIENT_TABLE_NAME = "clients";
|
|
public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID
|
|
public static final String TYPE_COLUMN = "type";
|
|
public static final String STATUS_COLUMN = "status";
|
|
public static final String LOCALE_COLUMN = "locale";
|
|
public static final String WORDLISTID_COLUMN = "id";
|
|
public static final String DESCRIPTION_COLUMN = "description";
|
|
public static final String LOCAL_FILENAME_COLUMN = "filename";
|
|
public static final String REMOTE_FILENAME_COLUMN = "url";
|
|
public static final String DATE_COLUMN = "date";
|
|
public static final String CHECKSUM_COLUMN = "checksum";
|
|
public static final String FILESIZE_COLUMN = "filesize";
|
|
public static final String VERSION_COLUMN = "version";
|
|
public static final String FORMATVERSION_COLUMN = "formatversion";
|
|
public static final String FLAGS_COLUMN = "flags";
|
|
public static final String RAW_CHECKSUM_COLUMN = "rawChecksum";
|
|
public static final String RETRY_COUNT_COLUMN = "remainingRetries";
|
|
public static final int COLUMN_COUNT = 15;
|
|
|
|
private static final String CLIENT_CLIENT_ID_COLUMN = "clientid";
|
|
private static final String CLIENT_METADATA_URI_COLUMN = "uri";
|
|
private static final String CLIENT_METADATA_ADDITIONAL_ID_COLUMN = "additionalid";
|
|
private static final String CLIENT_LAST_UPDATE_DATE_COLUMN = "lastupdate";
|
|
private static final String CLIENT_PENDINGID_COLUMN = "pendingid"; // Download Manager ID
|
|
|
|
public static final String METADATA_DATABASE_NAME_STEM = "pendingUpdates";
|
|
public static final String METADATA_UPDATE_DESCRIPTION = "metadata";
|
|
|
|
public static final String DICTIONARIES_ASSETS_PATH = "dictionaries";
|
|
|
|
// Statuses, for storing in the STATUS_COLUMN
|
|
// IMPORTANT: The following are used as index arrays in ../WordListPreference
|
|
// Do not change their values without updating the matched code.
|
|
// Unknown status: this should never happen.
|
|
public static final int STATUS_UNKNOWN = 0;
|
|
// Available: this word list is available, but it is not downloaded (not downloading), because
|
|
// it is set not to be used.
|
|
public static final int STATUS_AVAILABLE = 1;
|
|
// Downloading: this word list is being downloaded.
|
|
public static final int STATUS_DOWNLOADING = 2;
|
|
// Installed: this word list is installed and usable.
|
|
public static final int STATUS_INSTALLED = 3;
|
|
// Disabled: this word list is installed, but has been disabled by the user.
|
|
public static final int STATUS_DISABLED = 4;
|
|
// Deleting: the user marked this word list to be deleted, but it has not been yet because
|
|
// Latin IME is not up yet.
|
|
public static final int STATUS_DELETING = 5;
|
|
// Retry: dictionary got corrupted, so an attempt must be done to download & install it again.
|
|
public static final int STATUS_RETRYING = 6;
|
|
|
|
// Types, for storing in the TYPE_COLUMN
|
|
// This is metadata about what is available.
|
|
public static final int TYPE_METADATA = 1;
|
|
// This is a bulk file. It should replace older files.
|
|
public static final int TYPE_BULK = 2;
|
|
// This is an incremental update, expected to be small, and meaningless on its own.
|
|
public static final int TYPE_UPDATE = 3;
|
|
|
|
private static final String METADATA_TABLE_CREATE =
|
|
"CREATE TABLE " + METADATA_TABLE_NAME + " ("
|
|
+ PENDINGID_COLUMN + " INTEGER, "
|
|
+ TYPE_COLUMN + " INTEGER, "
|
|
+ STATUS_COLUMN + " INTEGER, "
|
|
+ WORDLISTID_COLUMN + " TEXT, "
|
|
+ LOCALE_COLUMN + " TEXT, "
|
|
+ DESCRIPTION_COLUMN + " TEXT, "
|
|
+ LOCAL_FILENAME_COLUMN + " TEXT, "
|
|
+ REMOTE_FILENAME_COLUMN + " TEXT, "
|
|
+ DATE_COLUMN + " INTEGER, "
|
|
+ CHECKSUM_COLUMN + " TEXT, "
|
|
+ FILESIZE_COLUMN + " INTEGER, "
|
|
+ VERSION_COLUMN + " INTEGER,"
|
|
+ FORMATVERSION_COLUMN + " INTEGER, "
|
|
+ FLAGS_COLUMN + " INTEGER, "
|
|
+ RAW_CHECKSUM_COLUMN + " TEXT,"
|
|
+ RETRY_COUNT_COLUMN + " INTEGER, "
|
|
+ "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));";
|
|
private static final String METADATA_CREATE_CLIENT_TABLE =
|
|
"CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " ("
|
|
+ CLIENT_CLIENT_ID_COLUMN + " TEXT, "
|
|
+ CLIENT_METADATA_URI_COLUMN + " TEXT, "
|
|
+ CLIENT_METADATA_ADDITIONAL_ID_COLUMN + " TEXT, "
|
|
+ CLIENT_LAST_UPDATE_DATE_COLUMN + " INTEGER NOT NULL DEFAULT 0, "
|
|
+ CLIENT_PENDINGID_COLUMN + " INTEGER, "
|
|
+ FLAGS_COLUMN + " INTEGER, "
|
|
+ "PRIMARY KEY (" + CLIENT_CLIENT_ID_COLUMN + "));";
|
|
|
|
// List of all metadata table columns.
|
|
static final String[] METADATA_TABLE_COLUMNS = { PENDINGID_COLUMN, TYPE_COLUMN,
|
|
STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN,
|
|
LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN,
|
|
FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN,
|
|
RAW_CHECKSUM_COLUMN, RETRY_COUNT_COLUMN };
|
|
// List of all client table columns.
|
|
static final String[] CLIENT_TABLE_COLUMNS = { CLIENT_CLIENT_ID_COLUMN,
|
|
CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN };
|
|
// List of public columns returned to clients. Everything that is not in this list is
|
|
// private and implementation-dependent.
|
|
static final String[] DICTIONARIES_LIST_PUBLIC_COLUMNS = { STATUS_COLUMN, WORDLISTID_COLUMN,
|
|
LOCALE_COLUMN, DESCRIPTION_COLUMN, DATE_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN };
|
|
|
|
// This class exhibits a singleton-like behavior by client ID, so it is getInstance'd
|
|
// and has a private c'tor.
|
|
private static TreeMap<String, MetadataDbHelper> sInstanceMap = null;
|
|
public static synchronized MetadataDbHelper getInstance(final Context context,
|
|
final String clientIdOrNull) {
|
|
// As a backward compatibility feature, null can be passed here to retrieve the "default"
|
|
// database. Before multi-client support, the dictionary packed used only one database
|
|
// and would not be able to handle several dictionary sets. Passing null here retrieves
|
|
// this legacy database. New clients should make sure to always pass a client ID so as
|
|
// to avoid conflicts.
|
|
final String clientId = null != clientIdOrNull ? clientIdOrNull : "";
|
|
if (null == sInstanceMap) sInstanceMap = new TreeMap<>();
|
|
MetadataDbHelper helper = sInstanceMap.get(clientId);
|
|
if (null == helper) {
|
|
helper = new MetadataDbHelper(context, clientId);
|
|
sInstanceMap.put(clientId, helper);
|
|
}
|
|
return helper;
|
|
}
|
|
private MetadataDbHelper(final Context context, final String clientId) {
|
|
super(context,
|
|
METADATA_DATABASE_NAME_STEM + (TextUtils.isEmpty(clientId) ? "" : "." + clientId),
|
|
null, CURRENT_METADATA_DATABASE_VERSION);
|
|
mContext = context;
|
|
mClientId = clientId;
|
|
}
|
|
|
|
private final Context mContext;
|
|
private final String mClientId;
|
|
|
|
/**
|
|
* Get the database itself. This always returns the same object for any client ID. If the
|
|
* client ID is null, a default database is returned for backward compatibility. Don't
|
|
* pass null for new calls.
|
|
*
|
|
* @param context the context to create the database from. This is ignored after the first call.
|
|
* @param clientId the client id to retrieve the database of. null for default (deprecated)
|
|
* @return the database.
|
|
*/
|
|
public static SQLiteDatabase getDb(final Context context, final String clientId) {
|
|
return getInstance(context, clientId).getWritableDatabase();
|
|
}
|
|
|
|
private void createClientTable(final SQLiteDatabase db) {
|
|
// The clients table only exists in the primary db, the one that has an empty client id
|
|
if (!TextUtils.isEmpty(mClientId)) return;
|
|
db.execSQL(METADATA_CREATE_CLIENT_TABLE);
|
|
final String defaultMetadataUri = mContext.getString(R.string.default_metadata_uri);
|
|
if (!TextUtils.isEmpty(defaultMetadataUri)) {
|
|
final ContentValues defaultMetadataValues = new ContentValues();
|
|
defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "");
|
|
defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri);
|
|
defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
|
|
db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the table and populate it with the resources found inside the apk.
|
|
*
|
|
* @see SQLiteOpenHelper#onCreate(SQLiteDatabase)
|
|
*
|
|
* @param db the database to create and populate.
|
|
*/
|
|
@Override
|
|
public void onCreate(final SQLiteDatabase db) {
|
|
db.execSQL(METADATA_TABLE_CREATE);
|
|
createClientTable(db);
|
|
}
|
|
|
|
private static void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db) {
|
|
try {
|
|
db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM "
|
|
+ METADATA_TABLE_NAME + " LIMIT 0;");
|
|
} catch (SQLiteException e) {
|
|
Log.i(TAG, "No " + RAW_CHECKSUM_COLUMN + " column : creating it");
|
|
db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN "
|
|
+ RAW_CHECKSUM_COLUMN + " TEXT;");
|
|
}
|
|
}
|
|
|
|
private static void addRetryCountColumnUnlessPresent(final SQLiteDatabase db) {
|
|
try {
|
|
db.execSQL("SELECT " + RETRY_COUNT_COLUMN + " FROM "
|
|
+ METADATA_TABLE_NAME + " LIMIT 0;");
|
|
} catch (SQLiteException e) {
|
|
Log.i(TAG, "No " + RETRY_COUNT_COLUMN + " column : creating it");
|
|
db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN "
|
|
+ RETRY_COUNT_COLUMN + " INTEGER DEFAULT " + DICTIONARY_RETRY_THRESHOLD + ";");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upgrade the database. Upgrade from version 3 is supported.
|
|
* Version 3 has a DB named METADATA_DATABASE_NAME_STEM containing a table METADATA_TABLE_NAME.
|
|
* Version 6 and above has a DB named METADATA_DATABASE_NAME_STEM containing a
|
|
* table CLIENT_TABLE_NAME, and for each client a table called METADATA_TABLE_STEM + "." + the
|
|
* name of the client and contains a table METADATA_TABLE_NAME.
|
|
* For schemas, see the above create statements. The schemas have never changed so far.
|
|
*
|
|
* This method is called by the framework. See {@link SQLiteOpenHelper#onUpgrade}
|
|
* @param db The database we are upgrading
|
|
* @param oldVersion The old database version (the one on the disk)
|
|
* @param newVersion The new database version as supplied to the constructor of SQLiteOpenHelper
|
|
*/
|
|
@Override
|
|
public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
|
|
if (METADATA_DATABASE_INITIAL_VERSION == oldVersion
|
|
&& METADATA_DATABASE_VERSION_WITH_CLIENTID <= newVersion
|
|
&& CURRENT_METADATA_DATABASE_VERSION >= newVersion) {
|
|
// Upgrade from version METADATA_DATABASE_INITIAL_VERSION to version
|
|
// METADATA_DATABASE_VERSION_WITH_CLIENT_ID
|
|
// Only the default database should contain the client table, so we test for mClientId.
|
|
if (TextUtils.isEmpty(mClientId)) {
|
|
// Anyway in version 3 only the default table existed so the emptiness
|
|
// test should always be true, but better check to be sure.
|
|
createClientTable(db);
|
|
}
|
|
} else if (METADATA_DATABASE_VERSION_WITH_CLIENTID < newVersion
|
|
&& CURRENT_METADATA_DATABASE_VERSION >= newVersion) {
|
|
// Here we drop the client table, so that all clients send us their information again.
|
|
// The client table contains the URL to hit to update the available dictionaries list,
|
|
// but the info about the dictionaries themselves is stored in the table called
|
|
// METADATA_TABLE_NAME and we want to keep it, so we only drop the client table.
|
|
db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME);
|
|
// Only the default database should contain the client table, so we test for mClientId.
|
|
if (TextUtils.isEmpty(mClientId)) {
|
|
createClientTable(db);
|
|
}
|
|
} else {
|
|
// If we're not in the above case, either we are upgrading from an earlier versionCode
|
|
// and we should wipe the database, or we are handling a version we never heard about
|
|
// (can only be a bug) so it's safer to wipe the database.
|
|
db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME);
|
|
db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME);
|
|
onCreate(db);
|
|
}
|
|
// A rawChecksum column that did not exist in the previous versions was added that
|
|
// corresponds to the md5 checksum of the file after decompression/decryption. This is to
|
|
// strengthen the system against corrupted dictionary files.
|
|
// The most secure way to upgrade a database is to just test for the column presence, and
|
|
// add it if it's not there.
|
|
addRawChecksumColumnUnlessPresent(db);
|
|
|
|
// A retry count column that did not exist in the previous versions was added that
|
|
// corresponds to the number of download & installation attempts that have been made
|
|
// in order to strengthen the system recovery from corrupted dictionary files.
|
|
// The most secure way to upgrade a database is to just test for the column presence, and
|
|
// add it if it's not there.
|
|
addRetryCountColumnUnlessPresent(db);
|
|
}
|
|
|
|
/**
|
|
* Downgrade the database. This drops and recreates the table in all cases.
|
|
*/
|
|
@Override
|
|
public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
|
|
// No matter what the numerical values of oldVersion and newVersion are, we know this
|
|
// is a downgrade (newVersion < oldVersion). There is no way to know what the future
|
|
// databases will look like, but we know it's extremely likely that it's okay to just
|
|
// drop the tables and start from scratch. Hence, we ignore the versions and just wipe
|
|
// everything we want to use.
|
|
if (oldVersion <= newVersion) {
|
|
Log.e(TAG, "onDowngrade database but new version is higher? " + oldVersion + " <= "
|
|
+ newVersion);
|
|
}
|
|
db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME);
|
|
db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME);
|
|
onCreate(db);
|
|
}
|
|
|
|
/**
|
|
* Given a client ID, returns whether this client exists.
|
|
*
|
|
* @param context a context to open the database
|
|
* @param clientId the client ID to check
|
|
* @return true if the client is known, false otherwise
|
|
*/
|
|
public static boolean isClientKnown(final Context context, final String clientId) {
|
|
// If the client is known, they'll have a non-null metadata URI. An empty string is
|
|
// allowed as a metadata URI, if the client doesn't want any updates to happen.
|
|
return null != getMetadataUriAsString(context, clientId);
|
|
}
|
|
|
|
private static final MetadataUriGetter sMetadataUriGetter = new MetadataUriGetter();
|
|
|
|
/**
|
|
* Returns the metadata URI as a string.
|
|
*
|
|
* If the client is not known, this will return null. If it is known, it will return
|
|
* the URI as a string. Note that the empty string is a valid value.
|
|
*
|
|
* @param context a context instance to open the database on
|
|
* @param clientId the ID of the client we want the metadata URI of
|
|
* @return the string representation of the URI
|
|
*/
|
|
public static String getMetadataUriAsString(final Context context, final String clientId) {
|
|
SQLiteDatabase defaultDb = MetadataDbHelper.getDb(context, null);
|
|
final Cursor cursor = defaultDb.query(MetadataDbHelper.CLIENT_TABLE_NAME,
|
|
new String[] { MetadataDbHelper.CLIENT_METADATA_URI_COLUMN },
|
|
MetadataDbHelper.CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId },
|
|
null, null, null, null);
|
|
try {
|
|
if (!cursor.moveToFirst()) return null;
|
|
return sMetadataUriGetter.getUri(context, cursor.getString(0));
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the last metadata update time for all clients using a particular URI.
|
|
*
|
|
* This method searches for all clients using a particular URI and updates the last
|
|
* update time for this client.
|
|
* The current time is used as the latest update time. This saved date will be what
|
|
* is returned henceforth by {@link #getLastUpdateDateForClient(Context, String)},
|
|
* until this method is called again.
|
|
*
|
|
* @param context a context instance to open the database on
|
|
* @param uri the metadata URI we just downloaded
|
|
*/
|
|
public static void saveLastUpdateTimeOfUri(final Context context, final String uri) {
|
|
PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis());
|
|
final ContentValues values = new ContentValues();
|
|
values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis());
|
|
final SQLiteDatabase defaultDb = getDb(context, null);
|
|
final Cursor cursor = MetadataDbHelper.queryClientIds(context);
|
|
if (null == cursor) return;
|
|
try {
|
|
if (!cursor.moveToFirst()) return;
|
|
do {
|
|
final String clientId = cursor.getString(0);
|
|
final String metadataUri =
|
|
MetadataDbHelper.getMetadataUriAsString(context, clientId);
|
|
if (metadataUri.equals(uri)) {
|
|
defaultDb.update(CLIENT_TABLE_NAME, values,
|
|
CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
|
|
}
|
|
} while (cursor.moveToNext());
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the last date at which we updated the metadata for this client.
|
|
*
|
|
* The returned date is in milliseconds from the EPOCH; this is the same unit as
|
|
* returned by {@link System#currentTimeMillis()}.
|
|
*
|
|
* @param context a context instance to open the database on
|
|
* @param clientId the client ID to get the latest update date of
|
|
* @return the last date at which this client was updated, as a long.
|
|
*/
|
|
public static long getLastUpdateDateForClient(final Context context, final String clientId) {
|
|
SQLiteDatabase defaultDb = getDb(context, null);
|
|
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
|
|
new String[] { CLIENT_LAST_UPDATE_DATE_COLUMN },
|
|
CLIENT_CLIENT_ID_COLUMN + " = ?",
|
|
new String[] { null == clientId ? "" : clientId },
|
|
null, null, null, null);
|
|
try {
|
|
if (!cursor.moveToFirst()) return 0;
|
|
return cursor.getLong(0); // Only one column, return it
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the metadata download ID for a metadata URI.
|
|
*
|
|
* This will retrieve the download ID for the metadata file that has the passed URI.
|
|
* If this URI is not being downloaded right now, it will return NOT_AN_ID.
|
|
*
|
|
* @param context a context instance to open the database on
|
|
* @param uri the URI to retrieve the metadata download ID of
|
|
* @return the download id and start date, or null if the URL is not known
|
|
*/
|
|
public static DownloadIdAndStartDate getMetadataDownloadIdAndStartDateForURI(
|
|
final Context context, final String uri) {
|
|
SQLiteDatabase defaultDb = getDb(context, null);
|
|
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
|
|
new String[] { CLIENT_PENDINGID_COLUMN, CLIENT_LAST_UPDATE_DATE_COLUMN },
|
|
CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri },
|
|
null, null, null, null);
|
|
try {
|
|
if (!cursor.moveToFirst()) return null;
|
|
return new DownloadIdAndStartDate(cursor.getInt(0), cursor.getLong(1));
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
public static long getOldestUpdateTime(final Context context) {
|
|
SQLiteDatabase defaultDb = getDb(context, null);
|
|
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
|
|
new String[] { CLIENT_LAST_UPDATE_DATE_COLUMN },
|
|
null, null, null, null, null);
|
|
try {
|
|
if (!cursor.moveToFirst()) return 0;
|
|
final int columnIndex = 0; // Only one column queried
|
|
// Initialize the earliestTime to the largest possible value.
|
|
long earliestTime = Long.MAX_VALUE; // Almost 300 million years in the future
|
|
do {
|
|
final long thisTime = cursor.getLong(columnIndex);
|
|
earliestTime = Math.min(thisTime, earliestTime);
|
|
} while (cursor.moveToNext());
|
|
return earliestTime;
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper method to make content values to write into the database.
|
|
* @return content values with all the arguments put with the right column names.
|
|
*/
|
|
public static ContentValues 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) {
|
|
final ContentValues result = new ContentValues(COLUMN_COUNT);
|
|
result.put(PENDINGID_COLUMN, pendingId);
|
|
result.put(TYPE_COLUMN, type);
|
|
result.put(WORDLISTID_COLUMN, wordlistId);
|
|
result.put(STATUS_COLUMN, status);
|
|
result.put(LOCALE_COLUMN, locale);
|
|
result.put(DESCRIPTION_COLUMN, description);
|
|
result.put(LOCAL_FILENAME_COLUMN, filename);
|
|
result.put(REMOTE_FILENAME_COLUMN, url);
|
|
result.put(DATE_COLUMN, date);
|
|
result.put(RAW_CHECKSUM_COLUMN, rawChecksum);
|
|
result.put(RETRY_COUNT_COLUMN, retryCount);
|
|
result.put(CHECKSUM_COLUMN, checksum);
|
|
result.put(FILESIZE_COLUMN, filesize);
|
|
result.put(VERSION_COLUMN, version);
|
|
result.put(FORMATVERSION_COLUMN, formatVersion);
|
|
result.put(FLAGS_COLUMN, 0);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Helper method to fill in an incomplete ContentValues with default values.
|
|
* A wordlist ID and a locale are required, otherwise BadFormatException is thrown.
|
|
* @return the same object that was passed in, completed with default values.
|
|
*/
|
|
public static ContentValues completeWithDefaultValues(final ContentValues result)
|
|
throws BadFormatException {
|
|
if (null == result.get(WORDLISTID_COLUMN) || null == result.get(LOCALE_COLUMN)) {
|
|
throw new BadFormatException();
|
|
}
|
|
// 0 for the pending id, because there is none
|
|
if (null == result.get(PENDINGID_COLUMN)) result.put(PENDINGID_COLUMN, 0);
|
|
// This is a binary blob of a dictionary
|
|
if (null == result.get(TYPE_COLUMN)) result.put(TYPE_COLUMN, TYPE_BULK);
|
|
// This word list is unknown, but it's present, else we wouldn't be here, so INSTALLED
|
|
if (null == result.get(STATUS_COLUMN)) result.put(STATUS_COLUMN, STATUS_INSTALLED);
|
|
// No description unless specified, because we can't guess it
|
|
if (null == result.get(DESCRIPTION_COLUMN)) result.put(DESCRIPTION_COLUMN, "");
|
|
// File name - this is an asset, so it works as an already deleted file.
|
|
// hence, we need to supply a non-existent file name. Anything will
|
|
// do as long as it returns false when tested with File#exist(), and
|
|
// the empty string does not, so it's set to "_".
|
|
if (null == result.get(LOCAL_FILENAME_COLUMN)) result.put(LOCAL_FILENAME_COLUMN, "_");
|
|
// No remote file name : this can't be downloaded. Unless specified.
|
|
if (null == result.get(REMOTE_FILENAME_COLUMN)) result.put(REMOTE_FILENAME_COLUMN, "");
|
|
// 0 for the update date : 1970/1/1. Unless specified.
|
|
if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0);
|
|
// Raw checksum unknown unless specified
|
|
if (null == result.get(RAW_CHECKSUM_COLUMN)) result.put(RAW_CHECKSUM_COLUMN, "");
|
|
// Retry column 0 unless specified
|
|
if (null == result.get(RETRY_COUNT_COLUMN)) result.put(RETRY_COUNT_COLUMN,
|
|
DICTIONARY_RETRY_THRESHOLD);
|
|
// Checksum unknown unless specified
|
|
if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, "");
|
|
// No filesize unless specified
|
|
if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0);
|
|
// Smallest possible version unless specified
|
|
if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1);
|
|
// Assume current format unless specified
|
|
if (null == result.get(FORMATVERSION_COLUMN))
|
|
result.put(FORMATVERSION_COLUMN, UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION);
|
|
// No flags unless specified
|
|
if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Reads a column in a Cursor as a String and stores it in a ContentValues object.
|
|
* @param result the ContentValues object to store the result in.
|
|
* @param cursor the Cursor to read the column from.
|
|
* @param columnId the column ID to read.
|
|
*/
|
|
private static void putStringResult(ContentValues result, Cursor cursor, String columnId) {
|
|
result.put(columnId, cursor.getString(cursor.getColumnIndex(columnId)));
|
|
}
|
|
|
|
/**
|
|
* Reads a column in a Cursor as an int and stores it in a ContentValues object.
|
|
* @param result the ContentValues object to store the result in.
|
|
* @param cursor the Cursor to read the column from.
|
|
* @param columnId the column ID to read.
|
|
*/
|
|
private static void putIntResult(ContentValues result, Cursor cursor, String columnId) {
|
|
result.put(columnId, cursor.getInt(cursor.getColumnIndex(columnId)));
|
|
}
|
|
|
|
private static ContentValues getFirstLineAsContentValues(final Cursor cursor) {
|
|
final ContentValues result;
|
|
if (cursor.moveToFirst()) {
|
|
result = new ContentValues(COLUMN_COUNT);
|
|
putIntResult(result, cursor, PENDINGID_COLUMN);
|
|
putIntResult(result, cursor, TYPE_COLUMN);
|
|
putIntResult(result, cursor, STATUS_COLUMN);
|
|
putStringResult(result, cursor, WORDLISTID_COLUMN);
|
|
putStringResult(result, cursor, LOCALE_COLUMN);
|
|
putStringResult(result, cursor, DESCRIPTION_COLUMN);
|
|
putStringResult(result, cursor, LOCAL_FILENAME_COLUMN);
|
|
putStringResult(result, cursor, REMOTE_FILENAME_COLUMN);
|
|
putIntResult(result, cursor, DATE_COLUMN);
|
|
putStringResult(result, cursor, RAW_CHECKSUM_COLUMN);
|
|
putStringResult(result, cursor, CHECKSUM_COLUMN);
|
|
putIntResult(result, cursor, RETRY_COUNT_COLUMN);
|
|
putIntResult(result, cursor, FILESIZE_COLUMN);
|
|
putIntResult(result, cursor, VERSION_COLUMN);
|
|
putIntResult(result, cursor, FORMATVERSION_COLUMN);
|
|
putIntResult(result, cursor, FLAGS_COLUMN);
|
|
if (cursor.moveToNext()) {
|
|
// TODO: print the second level of the stack to the log so that we know
|
|
// in which code path the error happened
|
|
Log.e(TAG, "Several SQL results when we expected only one!");
|
|
}
|
|
} else {
|
|
result = null;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Gets the info about as specific download, indexed by its DownloadManager ID.
|
|
* @param db the database to get the information from.
|
|
* @param id the DownloadManager id.
|
|
* @return metadata about this download. This returns all columns in the database.
|
|
*/
|
|
public static ContentValues getContentValuesByPendingId(final SQLiteDatabase db,
|
|
final long id) {
|
|
final Cursor cursor = db.query(METADATA_TABLE_NAME,
|
|
METADATA_TABLE_COLUMNS,
|
|
PENDINGID_COLUMN + "= ?",
|
|
new String[] { Long.toString(id) },
|
|
null, null, null);
|
|
if (null == cursor) {
|
|
return null;
|
|
}
|
|
try {
|
|
// There should never be more than one result. If because of some bug there are,
|
|
// returning only one result is the right thing to do, because we couldn't handle
|
|
// several anyway and we should still handle one.
|
|
return getFirstLineAsContentValues(cursor);
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the info about an installed OR deleting word list with a specified id.
|
|
*
|
|
* Basically, this is the word list that we want to return to Android Keyboard when
|
|
* it asks for a specific id.
|
|
*
|
|
* @param db the database to get the information from.
|
|
* @param id the word list ID.
|
|
* @return the metadata about this word list.
|
|
*/
|
|
public static ContentValues getInstalledOrDeletingWordListContentValuesByWordListId(
|
|
final SQLiteDatabase db, final String id) {
|
|
final Cursor cursor = db.query(METADATA_TABLE_NAME,
|
|
METADATA_TABLE_COLUMNS,
|
|
WORDLISTID_COLUMN + "=? AND (" + STATUS_COLUMN + "=? OR " + STATUS_COLUMN + "=?)",
|
|
new String[] { id, Integer.toString(STATUS_INSTALLED),
|
|
Integer.toString(STATUS_DELETING) },
|
|
null, null, null);
|
|
if (null == cursor) {
|
|
return null;
|
|
}
|
|
try {
|
|
// There should only be one result, but if there are several, we can't tell which
|
|
// is the best, so we just return the first one.
|
|
return getFirstLineAsContentValues(cursor);
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a specific download ID, return records for all pending downloads across all clients.
|
|
*
|
|
* If several clients use the same metadata URL, we know to only download it once, and
|
|
* dispatch the update process across all relevant clients when the download ends. This means
|
|
* several clients may share a single download ID if they share a metadata URI.
|
|
* The dispatching is done in
|
|
* {@link UpdateHandler#downloadFinished(Context, android.content.Intent)}, which
|
|
* finds out about the list of relevant clients by calling this method.
|
|
*
|
|
* @param context a context instance to open the databases
|
|
* @param downloadId the download ID to query about
|
|
* @return the list of records. Never null, but may be empty.
|
|
*/
|
|
public static ArrayList<DownloadRecord> getDownloadRecordsForDownloadId(final Context context,
|
|
final long downloadId) {
|
|
final SQLiteDatabase defaultDb = getDb(context, "");
|
|
final ArrayList<DownloadRecord> results = new ArrayList<>();
|
|
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, CLIENT_TABLE_COLUMNS,
|
|
null, null, null, null, null);
|
|
try {
|
|
if (!cursor.moveToFirst()) return results;
|
|
final int clientIdIndex = cursor.getColumnIndex(CLIENT_CLIENT_ID_COLUMN);
|
|
final int pendingIdColumn = cursor.getColumnIndex(CLIENT_PENDINGID_COLUMN);
|
|
do {
|
|
final long pendingId = cursor.getInt(pendingIdColumn);
|
|
final String clientId = cursor.getString(clientIdIndex);
|
|
if (pendingId == downloadId) {
|
|
results.add(new DownloadRecord(clientId, null));
|
|
}
|
|
final ContentValues valuesForThisClient =
|
|
getContentValuesByPendingId(getDb(context, clientId), downloadId);
|
|
if (null != valuesForThisClient) {
|
|
results.add(new DownloadRecord(clientId, valuesForThisClient));
|
|
}
|
|
} while (cursor.moveToNext());
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Gets the info about a specific word list.
|
|
*
|
|
* @param db the database to get the information from.
|
|
* @param id the word list ID.
|
|
* @param version the word list version.
|
|
* @return the metadata about this word list.
|
|
*/
|
|
@Nullable
|
|
public static ContentValues getContentValuesByWordListId(final SQLiteDatabase db,
|
|
final String id, final int version) {
|
|
final Cursor cursor = db.query(METADATA_TABLE_NAME,
|
|
METADATA_TABLE_COLUMNS,
|
|
WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ? AND "
|
|
+ FORMATVERSION_COLUMN + "<= ?",
|
|
new String[]
|
|
{ id,
|
|
Integer.toString(version),
|
|
Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION)
|
|
},
|
|
null /* groupBy */,
|
|
null /* having */,
|
|
FORMATVERSION_COLUMN + " DESC"/* orderBy */);
|
|
if (null == cursor) {
|
|
return null;
|
|
}
|
|
try {
|
|
// This is a lookup by primary key, so there can't be more than one result.
|
|
return getFirstLineAsContentValues(cursor);
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the info about the latest word list with an id.
|
|
*
|
|
* @param db the database to get the information from.
|
|
* @param id the word list ID.
|
|
* @return the metadata about the word list with this id and the latest version number.
|
|
*/
|
|
public static ContentValues getContentValuesOfLatestAvailableWordlistById(
|
|
final SQLiteDatabase db, final String id) {
|
|
final Cursor cursor = db.query(METADATA_TABLE_NAME,
|
|
METADATA_TABLE_COLUMNS,
|
|
WORDLISTID_COLUMN + "= ?",
|
|
new String[] { id }, null, null, VERSION_COLUMN + " DESC", "1");
|
|
if (null == cursor) {
|
|
return null;
|
|
}
|
|
try {
|
|
// Return the first result from the list of results.
|
|
return getFirstLineAsContentValues(cursor);
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the current metadata about INSTALLED, AVAILABLE or DELETING dictionaries.
|
|
*
|
|
* This odd method is tailored to the needs of
|
|
* DictionaryProvider#getDictionaryWordListsForContentUri, which needs the word list if
|
|
* it is:
|
|
* - INSTALLED: this should be returned to LatinIME if the file is still inside the dictionary
|
|
* pack, so that it can be copied. If the file is not there, it's been copied already and should
|
|
* not be returned, so getDictionaryWordListsForContentUri takes care of this.
|
|
* - DELETING: this should be returned to LatinIME so that it can actually delete the file.
|
|
* - AVAILABLE: this should not be returned, but should be checked for auto-installation.
|
|
*
|
|
* @param context the context for getting the database.
|
|
* @param clientId the client id for retrieving the database. null for default (deprecated)
|
|
* @return a cursor with metadata about usable dictionaries.
|
|
*/
|
|
public static Cursor queryInstalledOrDeletingOrAvailableDictionaryMetadata(
|
|
final Context context, final String clientId) {
|
|
// If clientId is null, we get the defaut DB (see #getInstance() for more about this)
|
|
final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME,
|
|
METADATA_TABLE_COLUMNS,
|
|
STATUS_COLUMN + " = ? OR " + STATUS_COLUMN + " = ? OR " + STATUS_COLUMN + " = ?",
|
|
new String[] { Integer.toString(STATUS_INSTALLED),
|
|
Integer.toString(STATUS_DELETING),
|
|
Integer.toString(STATUS_AVAILABLE) },
|
|
null, null, LOCALE_COLUMN);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Gets the current metadata about all dictionaries.
|
|
*
|
|
* This will retrieve the metadata about all dictionaries, including
|
|
* older files, or files not yet downloaded.
|
|
*
|
|
* @param context the context for getting the database.
|
|
* @param clientId the client id for retrieving the database. null for default (deprecated)
|
|
* @return a cursor with metadata about usable dictionaries.
|
|
*/
|
|
public static Cursor queryCurrentMetadata(final Context context, final String clientId) {
|
|
// If clientId is null, we get the defaut DB (see #getInstance() for more about this)
|
|
final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME,
|
|
METADATA_TABLE_COLUMNS, null, null, null, null, LOCALE_COLUMN);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Gets the list of all dictionaries known to the dictionary provider, with only public columns.
|
|
*
|
|
* This will retrieve information about all known dictionaries, and their status. As such,
|
|
* it will also return information about dictionaries on the server that have not been
|
|
* downloaded yet, but may be requested.
|
|
* This only returns public columns. It does not populate internal columns in the returned
|
|
* cursor.
|
|
* The value returned by this method is intended to be good to be returned directly for a
|
|
* request of the list of dictionaries by a client.
|
|
*
|
|
* @param context the context to read the database from.
|
|
* @param clientId the client id for retrieving the database. null for default (deprecated)
|
|
* @return a cursor that lists all available dictionaries and their metadata.
|
|
*/
|
|
public static Cursor queryDictionaries(final Context context, final String clientId) {
|
|
// If clientId is null, we get the defaut DB (see #getInstance() for more about this)
|
|
final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME,
|
|
DICTIONARIES_LIST_PUBLIC_COLUMNS,
|
|
// Filter out empty locales so as not to return auxiliary data, like a
|
|
// data line for downloading metadata:
|
|
MetadataDbHelper.LOCALE_COLUMN + " != ?", new String[] {""},
|
|
// TODO: Reinstate the following code for bulk, then implement partial updates
|
|
/* MetadataDbHelper.TYPE_COLUMN + " = ?",
|
|
new String[] { Integer.toString(MetadataDbHelper.TYPE_BULK) }, */
|
|
null, null, LOCALE_COLUMN);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Deletes all data associated with a client.
|
|
*
|
|
* @param context the context for opening the database
|
|
* @param clientId the ID of the client to delete.
|
|
* @return true if the client was successfully deleted, false otherwise.
|
|
*/
|
|
public static boolean deleteClient(final Context context, final String clientId) {
|
|
// Remove all metadata associated with this client
|
|
final SQLiteDatabase db = getDb(context, clientId);
|
|
db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME);
|
|
db.execSQL(METADATA_TABLE_CREATE);
|
|
// Remove this client's entry in the clients table
|
|
final SQLiteDatabase defaultDb = getDb(context, "");
|
|
if (0 == defaultDb.delete(CLIENT_TABLE_NAME,
|
|
CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId })) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Updates information relative to a specific client.
|
|
*
|
|
* Updatable information includes the metadata URI and the additional ID column. It may be
|
|
* expanded in the future.
|
|
* The passed values must include a client ID in the key CLIENT_CLIENT_ID_COLUMN, and it must
|
|
* be equal to the string passed as an argument for clientId. It may not be empty.
|
|
* The passed values must also include a non-null metadata URI in the
|
|
* CLIENT_METADATA_URI_COLUMN column, as well as a non-null additional ID in the
|
|
* CLIENT_METADATA_ADDITIONAL_ID_COLUMN. Both these strings may be empty.
|
|
* If any of the above is not complied with, this function returns without updating data.
|
|
*
|
|
* @param context the context, to open the database
|
|
* @param clientId the ID of the client to update
|
|
* @param values the values to update. Must conform to the protocol (see above)
|
|
*/
|
|
public static void updateClientInfo(final Context context, final String clientId,
|
|
final ContentValues values) {
|
|
// Sanity check the content values
|
|
final String valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN);
|
|
final String valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN);
|
|
final String valuesMetadataAdditionalId =
|
|
values.getAsString(CLIENT_METADATA_ADDITIONAL_ID_COLUMN);
|
|
// Empty string is a valid client ID, but external apps may not configure it, so disallow
|
|
// both null and empty string.
|
|
// Empty string is a valid metadata URI if the client does not want updates, so allow
|
|
// empty string but disallow null.
|
|
// Empty string is a valid additional ID so allow empty string but disallow null.
|
|
if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri
|
|
|| null == valuesMetadataAdditionalId) {
|
|
// We need all these columns to be filled in
|
|
DebugLogUtils.l("Missing parameter for updateClientInfo");
|
|
return;
|
|
}
|
|
if (!clientId.equals(valuesClientId)) {
|
|
// Mismatch! The client violates the protocol.
|
|
DebugLogUtils.l("Received an updateClientInfo request for ", clientId,
|
|
" but the values " + "contain a different ID : ", valuesClientId);
|
|
return;
|
|
}
|
|
// Default value for a pending ID is NOT_AN_ID
|
|
values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
|
|
final SQLiteDatabase defaultDb = getDb(context, "");
|
|
if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
|
|
defaultDb.update(CLIENT_TABLE_NAME, values,
|
|
CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the list of existing client IDs.
|
|
* @param context the context to open the database
|
|
* @return a cursor containing only one column, and one client ID per line.
|
|
*/
|
|
public static Cursor queryClientIds(final Context context) {
|
|
return getDb(context, null).query(CLIENT_TABLE_NAME,
|
|
new String[] { CLIENT_CLIENT_ID_COLUMN }, null, null, null, null, null);
|
|
}
|
|
|
|
/**
|
|
* Register a download ID for a specific metadata URI.
|
|
*
|
|
* This method should be called when a download for a metadata URI is starting. It will
|
|
* search for all clients using this metadata URI and will register for each of them
|
|
* the download ID into the database for later retrieval by
|
|
* {@link #getDownloadRecordsForDownloadId(Context, long)}.
|
|
*
|
|
* @param context a context for opening databases
|
|
* @param uri the metadata URI
|
|
* @param downloadId the download ID
|
|
*/
|
|
public static void registerMetadataDownloadId(final Context context, final String uri,
|
|
final long downloadId) {
|
|
final ContentValues values = new ContentValues();
|
|
values.put(CLIENT_PENDINGID_COLUMN, downloadId);
|
|
values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis());
|
|
final SQLiteDatabase defaultDb = getDb(context, "");
|
|
final Cursor cursor = MetadataDbHelper.queryClientIds(context);
|
|
if (null == cursor) return;
|
|
try {
|
|
if (!cursor.moveToFirst()) return;
|
|
do {
|
|
final String clientId = cursor.getString(0);
|
|
final String metadataUri =
|
|
MetadataDbHelper.getMetadataUriAsString(context, clientId);
|
|
if (metadataUri.equals(uri)) {
|
|
defaultDb.update(CLIENT_TABLE_NAME, values,
|
|
CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId });
|
|
}
|
|
} while (cursor.moveToNext());
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marks a downloading entry as having successfully downloaded and being installed.
|
|
*
|
|
* The metadata database contains information about ongoing processes, typically ongoing
|
|
* downloads. This marks such an entry as having finished and having installed successfully,
|
|
* so it becomes INSTALLED.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param r content values about the entry to mark as processed.
|
|
*/
|
|
public static void markEntryAsFinishedDownloadingAndInstalled(final SQLiteDatabase db,
|
|
final ContentValues r) {
|
|
switch (r.getAsInteger(TYPE_COLUMN)) {
|
|
case TYPE_BULK:
|
|
DebugLogUtils.l("Ended processing a wordlist");
|
|
// Updating a bulk word list is a three-step operation:
|
|
// - Add the new entry to the table
|
|
// - Remove the old entry from the table
|
|
// - Erase the old file
|
|
// We start by gathering the names of the files we should delete.
|
|
final List<String> filenames = new LinkedList<>();
|
|
final Cursor c = db.query(METADATA_TABLE_NAME,
|
|
new String[] { LOCAL_FILENAME_COLUMN },
|
|
LOCALE_COLUMN + " = ? AND " +
|
|
WORDLISTID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?",
|
|
new String[] { r.getAsString(LOCALE_COLUMN),
|
|
r.getAsString(WORDLISTID_COLUMN),
|
|
Integer.toString(STATUS_INSTALLED) },
|
|
null, null, null);
|
|
try {
|
|
if (c.moveToFirst()) {
|
|
// There should never be more than one file, but if there are, it's a bug
|
|
// and we should remove them all. I think it might happen if the power of
|
|
// the phone is suddenly cut during an update.
|
|
final int filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN);
|
|
do {
|
|
DebugLogUtils.l("Setting for removal", c.getString(filenameIndex));
|
|
filenames.add(c.getString(filenameIndex));
|
|
} while (c.moveToNext());
|
|
}
|
|
} finally {
|
|
c.close();
|
|
}
|
|
r.put(STATUS_COLUMN, STATUS_INSTALLED);
|
|
db.beginTransactionNonExclusive();
|
|
// Delete all old entries. There should never be any stalled entries, but if
|
|
// there are, this deletes them.
|
|
db.delete(METADATA_TABLE_NAME,
|
|
WORDLISTID_COLUMN + " = ?",
|
|
new String[] { r.getAsString(WORDLISTID_COLUMN) });
|
|
db.insert(METADATA_TABLE_NAME, null, r);
|
|
db.setTransactionSuccessful();
|
|
db.endTransaction();
|
|
for (String filename : filenames) {
|
|
try {
|
|
final File f = new File(filename);
|
|
f.delete();
|
|
} catch (SecurityException e) {
|
|
// No permissions to delete. Um. Can't do anything.
|
|
} // I don't think anything else can be thrown
|
|
}
|
|
break;
|
|
default:
|
|
// Unknown type: do nothing.
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a downloading entry from the database.
|
|
*
|
|
* This is invoked when a download fails. Either we tried to download, but
|
|
* we received a permanent failure and we should remove it, or we got manually
|
|
* cancelled and we should leave it at that.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the DownloadManager id of the file.
|
|
*/
|
|
public static void deleteDownloadingEntry(final SQLiteDatabase db, final long id) {
|
|
db.delete(METADATA_TABLE_NAME, PENDINGID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?",
|
|
new String[] { Long.toString(id), Integer.toString(STATUS_DOWNLOADING) });
|
|
}
|
|
|
|
/**
|
|
* Forcefully removes an entry from the database.
|
|
*
|
|
* This is invoked when a file is broken. The file has been downloaded, but Android
|
|
* Keyboard is telling us it could not open it.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the id of the word list.
|
|
* @param version the version of the word list.
|
|
*/
|
|
public static void deleteEntry(final SQLiteDatabase db, final String id, final int version) {
|
|
db.delete(METADATA_TABLE_NAME, WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?",
|
|
new String[] { id, Integer.toString(version) });
|
|
}
|
|
|
|
/**
|
|
* Internal method that sets the current status of an entry of the database.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the id of the word list.
|
|
* @param version the version of the word list.
|
|
* @param status the status to set the word list to.
|
|
* @param downloadId an optional download id to write, or NOT_A_DOWNLOAD_ID
|
|
*/
|
|
private static void markEntryAs(final SQLiteDatabase db, final String id,
|
|
final int version, final int status, final long downloadId) {
|
|
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version);
|
|
values.put(STATUS_COLUMN, status);
|
|
if (NOT_A_DOWNLOAD_ID != downloadId) {
|
|
values.put(MetadataDbHelper.PENDINGID_COLUMN, downloadId);
|
|
}
|
|
db.update(METADATA_TABLE_NAME, values,
|
|
WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?",
|
|
new String[] { id, Integer.toString(version) });
|
|
}
|
|
|
|
/**
|
|
* Writes the status column for the wordlist with this id as enabled. Typically this
|
|
* means the word list is currently disabled and we want to set its status to INSTALLED.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the id of the word list.
|
|
* @param version the version of the word list.
|
|
*/
|
|
public static void markEntryAsEnabled(final SQLiteDatabase db, final String id,
|
|
final int version) {
|
|
markEntryAs(db, id, version, STATUS_INSTALLED, NOT_A_DOWNLOAD_ID);
|
|
}
|
|
|
|
/**
|
|
* Writes the status column for the wordlist with this id as disabled. Typically this
|
|
* means the word list is currently installed and we want to set its status to DISABLED.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the id of the word list.
|
|
* @param version the version of the word list.
|
|
*/
|
|
public static void markEntryAsDisabled(final SQLiteDatabase db, final String id,
|
|
final int version) {
|
|
markEntryAs(db, id, version, STATUS_DISABLED, NOT_A_DOWNLOAD_ID);
|
|
}
|
|
|
|
/**
|
|
* Writes the status column for the wordlist with this id as available. This happens for
|
|
* example when a word list has been deleted but can be downloaded again.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the id of the word list.
|
|
* @param version the version of the word list.
|
|
*/
|
|
public static void markEntryAsAvailable(final SQLiteDatabase db, final String id,
|
|
final int version) {
|
|
markEntryAs(db, id, version, STATUS_AVAILABLE, NOT_A_DOWNLOAD_ID);
|
|
}
|
|
|
|
/**
|
|
* Writes the designated word list as downloadable, alongside with its download id.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the id of the word list.
|
|
* @param version the version of the word list.
|
|
* @param downloadId the download id.
|
|
*/
|
|
public static void markEntryAsDownloading(final SQLiteDatabase db, final String id,
|
|
final int version, final long downloadId) {
|
|
markEntryAs(db, id, version, STATUS_DOWNLOADING, downloadId);
|
|
}
|
|
|
|
/**
|
|
* Writes the designated word list as deleting.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the id of the word list.
|
|
* @param version the version of the word list.
|
|
*/
|
|
public static void markEntryAsDeleting(final SQLiteDatabase db, final String id,
|
|
final int version) {
|
|
markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID);
|
|
}
|
|
|
|
/**
|
|
* Checks retry counts and marks the word list as retrying if retry is possible.
|
|
*
|
|
* @param db the metadata database.
|
|
* @param id the id of the word list.
|
|
* @param version the version of the word list.
|
|
* @return {@code true} if the retry is possible.
|
|
*/
|
|
public static boolean maybeMarkEntryAsRetrying(final SQLiteDatabase db, final String id,
|
|
final int version) {
|
|
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version);
|
|
int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN);
|
|
if (retryCount > 1) {
|
|
values.put(STATUS_COLUMN, STATUS_RETRYING);
|
|
values.put(RETRY_COUNT_COLUMN, retryCount - 1);
|
|
db.update(METADATA_TABLE_NAME, values,
|
|
WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?",
|
|
new String[] { id, Integer.toString(version) });
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|