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