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