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