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