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