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 import com.android.inputmethod.latin.utils.DebugLogUtils; 29 30 import java.io.File; 31 import java.util.ArrayList; 32 import java.util.LinkedList; 33 import java.util.List; 34 import java.util.TreeMap; 35 36 /** 37 * Various helper functions for the state database 38 */ 39 public 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 // This is the current database version. It should be updated when the database schema 49 // gets updated. It is passed to the framework constructor of SQLiteOpenHelper, so 50 // that's what the framework uses to track our database version. 51 private static final int METADATA_DATABASE_VERSION = 6; 52 53 private final static long NOT_A_DOWNLOAD_ID = -1; 54 55 public static final String METADATA_TABLE_NAME = "pendingUpdates"; 56 static final String CLIENT_TABLE_NAME = "clients"; 57 public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID 58 public static final String TYPE_COLUMN = "type"; 59 public static final String STATUS_COLUMN = "status"; 60 public static final String LOCALE_COLUMN = "locale"; 61 public static final String WORDLISTID_COLUMN = "id"; 62 public static final String DESCRIPTION_COLUMN = "description"; 63 public static final String LOCAL_FILENAME_COLUMN = "filename"; 64 public static final String REMOTE_FILENAME_COLUMN = "url"; 65 public static final String DATE_COLUMN = "date"; 66 public static final String CHECKSUM_COLUMN = "checksum"; 67 public static final String FILESIZE_COLUMN = "filesize"; 68 public static final String VERSION_COLUMN = "version"; 69 public static final String FORMATVERSION_COLUMN = "formatversion"; 70 public static final String FLAGS_COLUMN = "flags"; 71 public static final int COLUMN_COUNT = 13; 72 73 private static final String CLIENT_CLIENT_ID_COLUMN = "clientid"; 74 private static final String CLIENT_METADATA_URI_COLUMN = "uri"; 75 private static final String CLIENT_METADATA_ADDITIONAL_ID_COLUMN = "additionalid"; 76 private static final String CLIENT_LAST_UPDATE_DATE_COLUMN = "lastupdate"; 77 private static final String CLIENT_PENDINGID_COLUMN = "pendingid"; // Download Manager ID 78 79 public static final String METADATA_DATABASE_NAME_STEM = "pendingUpdates"; 80 public static final String METADATA_UPDATE_DESCRIPTION = "metadata"; 81 82 public static final String DICTIONARIES_ASSETS_PATH = "dictionaries"; 83 84 // Statuses, for storing in the STATUS_COLUMN 85 // IMPORTANT: The following are used as index arrays in ../WordListPreference 86 // Do not change their values without updating the matched code. 87 // Unknown status: this should never happen. 88 public static final int STATUS_UNKNOWN = 0; 89 // Available: this word list is available, but it is not downloaded (not downloading), because 90 // it is set not to be used. 91 public static final int STATUS_AVAILABLE = 1; 92 // Downloading: this word list is being downloaded. 93 public static final int STATUS_DOWNLOADING = 2; 94 // Installed: this word list is installed and usable. 95 public static final int STATUS_INSTALLED = 3; 96 // Disabled: this word list is installed, but has been disabled by the user. 97 public static final int STATUS_DISABLED = 4; 98 // Deleting: the user marked this word list to be deleted, but it has not been yet because 99 // Latin IME is not up yet. 100 public static final int STATUS_DELETING = 5; 101 102 // Types, for storing in the TYPE_COLUMN 103 // This is metadata about what is available. 104 public static final int TYPE_METADATA = 1; 105 // This is a bulk file. It should replace older files. 106 public static final int TYPE_BULK = 2; 107 // This is an incremental update, expected to be small, and meaningless on its own. 108 public static final int TYPE_UPDATE = 3; 109 110 private static final String METADATA_TABLE_CREATE = 111 "CREATE TABLE " + METADATA_TABLE_NAME + " (" 112 + PENDINGID_COLUMN + " INTEGER, " 113 + TYPE_COLUMN + " INTEGER, " 114 + STATUS_COLUMN + " INTEGER, " 115 + WORDLISTID_COLUMN + " TEXT, " 116 + LOCALE_COLUMN + " TEXT, " 117 + DESCRIPTION_COLUMN + " TEXT, " 118 + LOCAL_FILENAME_COLUMN + " TEXT, " 119 + REMOTE_FILENAME_COLUMN + " TEXT, " 120 + DATE_COLUMN + " INTEGER, " 121 + CHECKSUM_COLUMN + " TEXT, " 122 + FILESIZE_COLUMN + " INTEGER, " 123 + VERSION_COLUMN + " INTEGER," 124 + FORMATVERSION_COLUMN + " INTEGER," 125 + FLAGS_COLUMN + " INTEGER," 126 + "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));"; 127 private static final String METADATA_CREATE_CLIENT_TABLE = 128 "CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " (" 129 + CLIENT_CLIENT_ID_COLUMN + " TEXT, " 130 + CLIENT_METADATA_URI_COLUMN + " TEXT, " 131 + CLIENT_METADATA_ADDITIONAL_ID_COLUMN + " TEXT, " 132 + CLIENT_LAST_UPDATE_DATE_COLUMN + " INTEGER NOT NULL DEFAULT 0, " 133 + CLIENT_PENDINGID_COLUMN + " INTEGER, " 134 + FLAGS_COLUMN + " INTEGER, " 135 + "PRIMARY KEY (" + CLIENT_CLIENT_ID_COLUMN + "));"; 136 137 // List of all metadata table columns. 138 static final String[] METADATA_TABLE_COLUMNS = { PENDINGID_COLUMN, TYPE_COLUMN, 139 STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN, 140 LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN, 141 FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN }; 142 // List of all client table columns. 143 static final String[] CLIENT_TABLE_COLUMNS = { CLIENT_CLIENT_ID_COLUMN, 144 CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN }; 145 // List of public columns returned to clients. Everything that is not in this list is 146 // private and implementation-dependent. 147 static final String[] DICTIONARIES_LIST_PUBLIC_COLUMNS = { STATUS_COLUMN, WORDLISTID_COLUMN, 148 LOCALE_COLUMN, DESCRIPTION_COLUMN, DATE_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN }; 149 150 // This class exhibits a singleton-like behavior by client ID, so it is getInstance'd 151 // and has a private c'tor. 152 private static TreeMap<String, MetadataDbHelper> sInstanceMap = null; 153 public static synchronized MetadataDbHelper getInstance(final Context context, 154 final String clientIdOrNull) { 155 // As a backward compatibility feature, null can be passed here to retrieve the "default" 156 // database. Before multi-client support, the dictionary packed used only one database 157 // and would not be able to handle several dictionary sets. Passing null here retrieves 158 // this legacy database. New clients should make sure to always pass a client ID so as 159 // to avoid conflicts. 160 final String clientId = null != clientIdOrNull ? clientIdOrNull : ""; 161 if (null == sInstanceMap) sInstanceMap = new TreeMap<String, MetadataDbHelper>(); 162 MetadataDbHelper helper = sInstanceMap.get(clientId); 163 if (null == helper) { 164 helper = new MetadataDbHelper(context, clientId); 165 sInstanceMap.put(clientId, helper); 166 } 167 return helper; 168 } 169 private MetadataDbHelper(final Context context, final String clientId) { 170 super(context, 171 METADATA_DATABASE_NAME_STEM + (TextUtils.isEmpty(clientId) ? "" : "." + clientId), 172 null, METADATA_DATABASE_VERSION); 173 mContext = context; 174 mClientId = clientId; 175 } 176 177 private final Context mContext; 178 private final String mClientId; 179 180 /** 181 * Get the database itself. This always returns the same object for any client ID. If the 182 * client ID is null, a default database is returned for backward compatibility. Don't 183 * pass null for new calls. 184 * 185 * @param context the context to create the database from. This is ignored after the first call. 186 * @param clientId the client id to retrieve the database of. null for default (deprecated) 187 * @return the database. 188 */ 189 public static SQLiteDatabase getDb(final Context context, final String clientId) { 190 return getInstance(context, clientId).getWritableDatabase(); 191 } 192 193 private void createClientTable(final SQLiteDatabase db) { 194 // The clients table only exists in the primary db, the one that has an empty client id 195 if (!TextUtils.isEmpty(mClientId)) return; 196 db.execSQL(METADATA_CREATE_CLIENT_TABLE); 197 final String defaultMetadataUri = mContext.getString(R.string.default_metadata_uri); 198 if (!TextUtils.isEmpty(defaultMetadataUri)) { 199 final ContentValues defaultMetadataValues = new ContentValues(); 200 defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, ""); 201 defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri); 202 defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID); 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 metadata URI. 363 * 364 * This will retrieve the download ID for the metadata file that has the passed URI. 365 * If this URI is not being downloaded right now, it will return NOT_AN_ID. 366 * 367 * @param context a context instance to open the database on 368 * @param uri the URI 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 getMetadataDownloadIdForURI(final Context context, 372 final String uri) { 373 SQLiteDatabase defaultDb = getDb(context, null); 374 final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, 375 new String[] { CLIENT_PENDINGID_COLUMN }, 376 CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri }, 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 (null == result.get(WORDLISTID_COLUMN) || null == result.get(LOCALE_COLUMN)) { 441 throw new BadFormatException(); 442 } 443 // 0 for the pending id, because there is none 444 if (null == result.get(PENDINGID_COLUMN)) result.put(PENDINGID_COLUMN, 0); 445 // This is a binary blob of a dictionary 446 if (null == result.get(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 (null == result.get(STATUS_COLUMN)) result.put(STATUS_COLUMN, STATUS_INSTALLED); 449 // No description unless specified, because we can't guess it 450 if (null == result.get(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 (null == result.get(LOCAL_FILENAME_COLUMN)) result.put(LOCAL_FILENAME_COLUMN, "_"); 456 // No remote file name : this can't be downloaded. Unless specified. 457 if (null == result.get(REMOTE_FILENAME_COLUMN)) result.put(REMOTE_FILENAME_COLUMN, ""); 458 // 0 for the update date : 1970/1/1. Unless specified. 459 if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0); 460 // Checksum unknown unless specified 461 if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, ""); 462 // No filesize unless specified 463 if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0); 464 // Smallest possible version unless specified 465 if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1); 466 // Assume current format unless specified 467 if (null == result.get(FORMATVERSION_COLUMN)) 468 result.put(FORMATVERSION_COLUMN, UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION); 469 // No flags unless specified 470 if (null == result.get(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 576 * {@link UpdateHandler#downloadFinished(Context, android.content.Intent)}, which 577 * finds out about the list of relevant clients by calling this method. 578 * 579 * @param context a context instance to open the databases 580 * @param downloadId the download ID to query about 581 * @return the list of records. Never null, but may be empty. 582 */ 583 public static ArrayList<DownloadRecord> getDownloadRecordsForDownloadId(final Context context, 584 final long downloadId) { 585 final SQLiteDatabase defaultDb = getDb(context, ""); 586 final ArrayList<DownloadRecord> results = new ArrayList<DownloadRecord>(); 587 final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, CLIENT_TABLE_COLUMNS, 588 null, null, null, null, null); 589 try { 590 if (!cursor.moveToFirst()) return results; 591 final int clientIdIndex = cursor.getColumnIndex(CLIENT_CLIENT_ID_COLUMN); 592 final int pendingIdColumn = cursor.getColumnIndex(CLIENT_PENDINGID_COLUMN); 593 do { 594 final long pendingId = cursor.getInt(pendingIdColumn); 595 final String clientId = cursor.getString(clientIdIndex); 596 if (pendingId == downloadId) { 597 results.add(new DownloadRecord(clientId, null)); 598 } 599 final ContentValues valuesForThisClient = 600 getContentValuesByPendingId(getDb(context, clientId), downloadId); 601 if (null != valuesForThisClient) { 602 results.add(new DownloadRecord(clientId, valuesForThisClient)); 603 } 604 } while (cursor.moveToNext()); 605 } finally { 606 cursor.close(); 607 } 608 return results; 609 } 610 611 /** 612 * Gets the info about a specific word list. 613 * 614 * @param db the database to get the information from. 615 * @param id the word list ID. 616 * @param version the word list version. 617 * @return the metadata about this word list. 618 */ 619 public static ContentValues getContentValuesByWordListId(final SQLiteDatabase db, 620 final String id, final int version) { 621 final Cursor cursor = db.query(METADATA_TABLE_NAME, 622 METADATA_TABLE_COLUMNS, 623 WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ?", 624 new String[] { id, Integer.toString(version) }, null, null, null); 625 // This is a lookup by primary key, so there can't be more than one result. 626 final ContentValues result = getFirstLineAsContentValues(cursor); 627 cursor.close(); 628 return result; 629 } 630 631 /** 632 * Gets the info about the latest word list with an id. 633 * 634 * @param db the database to get the information from. 635 * @param id the word list ID. 636 * @return the metadata about the word list with this id and the latest version number. 637 */ 638 public static ContentValues getContentValuesOfLatestAvailableWordlistById( 639 final SQLiteDatabase db, final String id) { 640 final Cursor cursor = db.query(METADATA_TABLE_NAME, 641 METADATA_TABLE_COLUMNS, 642 WORDLISTID_COLUMN + "= ?", 643 new String[] { id }, null, null, VERSION_COLUMN + " DESC", "1"); 644 // This is a lookup by primary key, so there can't be more than one result. 645 final ContentValues result = getFirstLineAsContentValues(cursor); 646 cursor.close(); 647 return result; 648 } 649 650 /** 651 * Gets the current metadata about INSTALLED, AVAILABLE or DELETING dictionaries. 652 * 653 * This odd method is tailored to the needs of 654 * DictionaryProvider#getDictionaryWordListsForContentUri, which needs the word list if 655 * it is: 656 * - INSTALLED: this should be returned to LatinIME if the file is still inside the dictionary 657 * pack, so that it can be copied. If the file is not there, it's been copied already and should 658 * not be returned, so getDictionaryWordListsForContentUri takes care of this. 659 * - DELETING: this should be returned to LatinIME so that it can actually delete the file. 660 * - AVAILABLE: this should not be returned, but should be checked for auto-installation. 661 * 662 * @param context the context for getting the database. 663 * @param clientId the client id for retrieving the database. null for default (deprecated) 664 * @return a cursor with metadata about usable dictionaries. 665 */ 666 public static Cursor queryInstalledOrDeletingOrAvailableDictionaryMetadata( 667 final Context context, final String clientId) { 668 // If clientId is null, we get the defaut DB (see #getInstance() for more about this) 669 final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME, 670 METADATA_TABLE_COLUMNS, 671 STATUS_COLUMN + " = ? OR " + STATUS_COLUMN + " = ? OR " + STATUS_COLUMN + " = ?", 672 new String[] { Integer.toString(STATUS_INSTALLED), 673 Integer.toString(STATUS_DELETING), 674 Integer.toString(STATUS_AVAILABLE) }, 675 null, null, LOCALE_COLUMN); 676 return results; 677 } 678 679 /** 680 * Gets the current metadata about all dictionaries. 681 * 682 * This will retrieve the metadata about all dictionaries, including 683 * older files, or files not yet downloaded. 684 * 685 * @param context the context for getting the database. 686 * @param clientId the client id for retrieving the database. null for default (deprecated) 687 * @return a cursor with metadata about usable dictionaries. 688 */ 689 public static Cursor queryCurrentMetadata(final Context context, final String clientId) { 690 // If clientId is null, we get the defaut DB (see #getInstance() for more about this) 691 final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME, 692 METADATA_TABLE_COLUMNS, null, null, null, null, LOCALE_COLUMN); 693 return results; 694 } 695 696 /** 697 * Gets the list of all dictionaries known to the dictionary provider, with only public columns. 698 * 699 * This will retrieve information about all known dictionaries, and their status. As such, 700 * it will also return information about dictionaries on the server that have not been 701 * downloaded yet, but may be requested. 702 * This only returns public columns. It does not populate internal columns in the returned 703 * cursor. 704 * The value returned by this method is intended to be good to be returned directly for a 705 * request of the list of dictionaries by a client. 706 * 707 * @param context the context to read the database from. 708 * @param clientId the client id for retrieving the database. null for default (deprecated) 709 * @return a cursor that lists all available dictionaries and their metadata. 710 */ 711 public static Cursor queryDictionaries(final Context context, final String clientId) { 712 // If clientId is null, we get the defaut DB (see #getInstance() for more about this) 713 final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME, 714 DICTIONARIES_LIST_PUBLIC_COLUMNS, 715 // Filter out empty locales so as not to return auxiliary data, like a 716 // data line for downloading metadata: 717 MetadataDbHelper.LOCALE_COLUMN + " != ?", new String[] {""}, 718 // TODO: Reinstate the following code for bulk, then implement partial updates 719 /* MetadataDbHelper.TYPE_COLUMN + " = ?", 720 new String[] { Integer.toString(MetadataDbHelper.TYPE_BULK) }, */ 721 null, null, LOCALE_COLUMN); 722 return results; 723 } 724 725 /** 726 * Deletes all data associated with a client. 727 * 728 * @param context the context for opening the database 729 * @param clientId the ID of the client to delete. 730 * @return true if the client was successfully deleted, false otherwise. 731 */ 732 public static boolean deleteClient(final Context context, final String clientId) { 733 // Remove all metadata associated with this client 734 final SQLiteDatabase db = getDb(context, clientId); 735 db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME); 736 db.execSQL(METADATA_TABLE_CREATE); 737 // Remove this client's entry in the clients table 738 final SQLiteDatabase defaultDb = getDb(context, ""); 739 if (0 == defaultDb.delete(CLIENT_TABLE_NAME, 740 CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId })) { 741 return false; 742 } 743 return true; 744 } 745 746 /** 747 * Updates information relative to a specific client. 748 * 749 * Updatable information includes the metadata URI and the additional ID column. It may be 750 * expanded in the future. 751 * The passed values must include a client ID in the key CLIENT_CLIENT_ID_COLUMN, and it must 752 * be equal to the string passed as an argument for clientId. It may not be empty. 753 * The passed values must also include a non-null metadata URI in the 754 * CLIENT_METADATA_URI_COLUMN column, as well as a non-null additional ID in the 755 * CLIENT_METADATA_ADDITIONAL_ID_COLUMN. Both these strings may be empty. 756 * If any of the above is not complied with, this function returns without updating data. 757 * 758 * @param context the context, to open the database 759 * @param clientId the ID of the client to update 760 * @param values the values to update. Must conform to the protocol (see above) 761 */ 762 public static void updateClientInfo(final Context context, final String clientId, 763 final ContentValues values) { 764 // Sanity check the content values 765 final String valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN); 766 final String valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN); 767 final String valuesMetadataAdditionalId = 768 values.getAsString(CLIENT_METADATA_ADDITIONAL_ID_COLUMN); 769 // Empty string is a valid client ID, but external apps may not configure it, so disallow 770 // both null and empty string. 771 // Empty string is a valid metadata URI if the client does not want updates, so allow 772 // empty string but disallow null. 773 // Empty string is a valid additional ID so allow empty string but disallow null. 774 if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri 775 || null == valuesMetadataAdditionalId) { 776 // We need all these columns to be filled in 777 DebugLogUtils.l("Missing parameter for updateClientInfo"); 778 return; 779 } 780 if (!clientId.equals(valuesClientId)) { 781 // Mismatch! The client violates the protocol. 782 DebugLogUtils.l("Received an updateClientInfo request for ", clientId, 783 " but the values " + "contain a different ID : ", valuesClientId); 784 return; 785 } 786 // Default value for a pending ID is NOT_AN_ID 787 values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID); 788 final SQLiteDatabase defaultDb = getDb(context, ""); 789 if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) { 790 defaultDb.update(CLIENT_TABLE_NAME, values, 791 CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId }); 792 } 793 } 794 795 /** 796 * Retrieves the list of existing client IDs. 797 * @param context the context to open the database 798 * @return a cursor containing only one column, and one client ID per line. 799 */ 800 public static Cursor queryClientIds(final Context context) { 801 return getDb(context, null).query(CLIENT_TABLE_NAME, 802 new String[] { CLIENT_CLIENT_ID_COLUMN }, null, null, null, null, null); 803 } 804 805 /** 806 * Register a download ID for a specific metadata URI. 807 * 808 * This method should be called when a download for a metadata URI is starting. It will 809 * search for all clients using this metadata URI and will register for each of them 810 * the download ID into the database for later retrieval by 811 * {@link #getDownloadRecordsForDownloadId(Context, long)}. 812 * 813 * @param context a context for opening databases 814 * @param uri the metadata URI 815 * @param downloadId the download ID 816 */ 817 public static void registerMetadataDownloadId(final Context context, final String uri, 818 final long downloadId) { 819 final ContentValues values = new ContentValues(); 820 values.put(CLIENT_PENDINGID_COLUMN, downloadId); 821 final SQLiteDatabase defaultDb = getDb(context, ""); 822 final Cursor cursor = MetadataDbHelper.queryClientIds(context); 823 if (null == cursor) return; 824 try { 825 if (!cursor.moveToFirst()) return; 826 do { 827 final String clientId = cursor.getString(0); 828 final String metadataUri = 829 MetadataDbHelper.getMetadataUriAsString(context, clientId); 830 if (metadataUri.equals(uri)) { 831 defaultDb.update(CLIENT_TABLE_NAME, values, 832 CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId }); 833 } 834 } while (cursor.moveToNext()); 835 } finally { 836 cursor.close(); 837 } 838 } 839 840 /** 841 * Marks a downloading entry as having successfully downloaded and being installed. 842 * 843 * The metadata database contains information about ongoing processes, typically ongoing 844 * downloads. This marks such an entry as having finished and having installed successfully, 845 * so it becomes INSTALLED. 846 * 847 * @param db the metadata database. 848 * @param r content values about the entry to mark as processed. 849 */ 850 public static void markEntryAsFinishedDownloadingAndInstalled(final SQLiteDatabase db, 851 final ContentValues r) { 852 switch (r.getAsInteger(TYPE_COLUMN)) { 853 case TYPE_BULK: 854 DebugLogUtils.l("Ended processing a wordlist"); 855 // Updating a bulk word list is a three-step operation: 856 // - Add the new entry to the table 857 // - Remove the old entry from the table 858 // - Erase the old file 859 // We start by gathering the names of the files we should delete. 860 final List<String> filenames = new LinkedList<String>(); 861 final Cursor c = db.query(METADATA_TABLE_NAME, 862 new String[] { LOCAL_FILENAME_COLUMN }, 863 LOCALE_COLUMN + " = ? AND " + 864 WORDLISTID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?", 865 new String[] { r.getAsString(LOCALE_COLUMN), 866 r.getAsString(WORDLISTID_COLUMN), 867 Integer.toString(STATUS_INSTALLED) }, 868 null, null, null); 869 try { 870 if (c.moveToFirst()) { 871 // There should never be more than one file, but if there are, it's a bug 872 // and we should remove them all. I think it might happen if the power of 873 // the phone is suddenly cut during an update. 874 final int filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN); 875 do { 876 DebugLogUtils.l("Setting for removal", c.getString(filenameIndex)); 877 filenames.add(c.getString(filenameIndex)); 878 } while (c.moveToNext()); 879 } 880 } finally { 881 c.close(); 882 } 883 r.put(STATUS_COLUMN, STATUS_INSTALLED); 884 db.beginTransactionNonExclusive(); 885 // Delete all old entries. There should never be any stalled entries, but if 886 // there are, this deletes them. 887 db.delete(METADATA_TABLE_NAME, 888 WORDLISTID_COLUMN + " = ?", 889 new String[] { r.getAsString(WORDLISTID_COLUMN) }); 890 db.insert(METADATA_TABLE_NAME, null, r); 891 db.setTransactionSuccessful(); 892 db.endTransaction(); 893 for (String filename : filenames) { 894 try { 895 final File f = new File(filename); 896 f.delete(); 897 } catch (SecurityException e) { 898 // No permissions to delete. Um. Can't do anything. 899 } // I don't think anything else can be thrown 900 } 901 break; 902 default: 903 // Unknown type: do nothing. 904 break; 905 } 906 } 907 908 /** 909 * Removes a downloading entry from the database. 910 * 911 * This is invoked when a download fails. Either we tried to download, but 912 * we received a permanent failure and we should remove it, or we got manually 913 * cancelled and we should leave it at that. 914 * 915 * @param db the metadata database. 916 * @param id the DownloadManager id of the file. 917 */ 918 public static void deleteDownloadingEntry(final SQLiteDatabase db, final long id) { 919 db.delete(METADATA_TABLE_NAME, PENDINGID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?", 920 new String[] { Long.toString(id), Integer.toString(STATUS_DOWNLOADING) }); 921 } 922 923 /** 924 * Forcefully removes an entry from the database. 925 * 926 * This is invoked when a file is broken. The file has been downloaded, but Android 927 * Keyboard is telling us it could not open it. 928 * 929 * @param db the metadata database. 930 * @param id the id of the word list. 931 * @param version the version of the word list. 932 */ 933 public static void deleteEntry(final SQLiteDatabase db, final String id, final int version) { 934 db.delete(METADATA_TABLE_NAME, WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?", 935 new String[] { id, Integer.toString(version) }); 936 } 937 938 /** 939 * Internal method that sets the current status of an entry of the database. 940 * 941 * @param db the metadata database. 942 * @param id the id of the word list. 943 * @param version the version of the word list. 944 * @param status the status to set the word list to. 945 * @param downloadId an optional download id to write, or NOT_A_DOWNLOAD_ID 946 */ 947 private static void markEntryAs(final SQLiteDatabase db, final String id, 948 final int version, final int status, final long downloadId) { 949 final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version); 950 values.put(STATUS_COLUMN, status); 951 if (NOT_A_DOWNLOAD_ID != downloadId) { 952 values.put(MetadataDbHelper.PENDINGID_COLUMN, downloadId); 953 } 954 db.update(METADATA_TABLE_NAME, values, 955 WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?", 956 new String[] { id, Integer.toString(version) }); 957 } 958 959 /** 960 * Writes the status column for the wordlist with this id as enabled. Typically this 961 * means the word list is currently disabled and we want to set its status to INSTALLED. 962 * 963 * @param db the metadata database. 964 * @param id the id of the word list. 965 * @param version the version of the word list. 966 */ 967 public static void markEntryAsEnabled(final SQLiteDatabase db, final String id, 968 final int version) { 969 markEntryAs(db, id, version, STATUS_INSTALLED, NOT_A_DOWNLOAD_ID); 970 } 971 972 /** 973 * Writes the status column for the wordlist with this id as disabled. Typically this 974 * means the word list is currently installed and we want to set its status to DISABLED. 975 * 976 * @param db the metadata database. 977 * @param id the id of the word list. 978 * @param version the version of the word list. 979 */ 980 public static void markEntryAsDisabled(final SQLiteDatabase db, final String id, 981 final int version) { 982 markEntryAs(db, id, version, STATUS_DISABLED, NOT_A_DOWNLOAD_ID); 983 } 984 985 /** 986 * Writes the status column for the wordlist with this id as available. This happens for 987 * example when a word list has been deleted but can be downloaded again. 988 * 989 * @param db the metadata database. 990 * @param id the id of the word list. 991 * @param version the version of the word list. 992 */ 993 public static void markEntryAsAvailable(final SQLiteDatabase db, final String id, 994 final int version) { 995 markEntryAs(db, id, version, STATUS_AVAILABLE, NOT_A_DOWNLOAD_ID); 996 } 997 998 /** 999 * Writes the designated word list as downloadable, alongside with its download id. 1000 * 1001 * @param db the metadata database. 1002 * @param id the id of the word list. 1003 * @param version the version of the word list. 1004 * @param downloadId the download id. 1005 */ 1006 public static void markEntryAsDownloading(final SQLiteDatabase db, final String id, 1007 final int version, final long downloadId) { 1008 markEntryAs(db, id, version, STATUS_DOWNLOADING, downloadId); 1009 } 1010 1011 /** 1012 * Writes the designated word list as deleting. 1013 * 1014 * @param db the metadata database. 1015 * @param id the id of the word list. 1016 * @param version the version of the word list. 1017 */ 1018 public static void markEntryAsDeleting(final SQLiteDatabase db, final String id, 1019 final int version) { 1020 markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID); 1021 } 1022 } 1023