1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.webkit; 18 19 import java.util.ArrayList; 20 import java.util.HashMap; 21 import java.util.Iterator; 22 import java.util.List; 23 import java.util.Set; 24 import java.util.Map.Entry; 25 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.database.Cursor; 29 import android.database.DatabaseUtils; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.database.sqlite.SQLiteException; 32 import android.database.sqlite.SQLiteStatement; 33 import android.util.Log; 34 35 final class WebViewDatabaseClassic extends WebViewDatabase { 36 private static final String LOGTAG = "WebViewDatabaseClassic"; 37 private static final String DATABASE_FILE = "webview.db"; 38 private static final String CACHE_DATABASE_FILE = "webviewCache.db"; 39 40 private static final int DATABASE_VERSION = 11; 41 // 2 -> 3 Modified Cache table to allow cache of redirects 42 // 3 -> 4 Added Oma-Downloads table 43 // 4 -> 5 Modified Cache table to support persistent contentLength 44 // 5 -> 4 Removed Oma-Downoads table 45 // 5 -> 6 Add INDEX for cache table 46 // 6 -> 7 Change cache localPath from int to String 47 // 7 -> 8 Move cache to its own db 48 // 8 -> 9 Store both scheme and host when storing passwords 49 // 9 -> 10 Update httpauth table UNIQUE 50 // 10 -> 11 Drop cookies and cache now managed by the chromium stack, 51 // and update the form data table to use the new format 52 // implemented for b/5265606. 53 54 private static WebViewDatabaseClassic sInstance = null; 55 56 private static SQLiteDatabase sDatabase = null; 57 58 // synchronize locks 59 private final Object mPasswordLock = new Object(); 60 private final Object mFormLock = new Object(); 61 private final Object mHttpAuthLock = new Object(); 62 63 private static final String mTableNames[] = { 64 "password", "formurl", "formdata", "httpauth" 65 }; 66 67 // Table ids (they are index to mTableNames) 68 private static final int TABLE_PASSWORD_ID = 0; 69 private static final int TABLE_FORMURL_ID = 1; 70 private static final int TABLE_FORMDATA_ID = 2; 71 private static final int TABLE_HTTPAUTH_ID = 3; 72 73 // column id strings for "_id" which can be used by any table 74 private static final String ID_COL = "_id"; 75 76 private static final String[] ID_PROJECTION = new String[] { 77 "_id" 78 }; 79 80 // column id strings for "password" table 81 private static final String PASSWORD_HOST_COL = "host"; 82 private static final String PASSWORD_USERNAME_COL = "username"; 83 private static final String PASSWORD_PASSWORD_COL = "password"; 84 85 // column id strings for "formurl" table 86 private static final String FORMURL_URL_COL = "url"; 87 88 // column id strings for "formdata" table 89 private static final String FORMDATA_URLID_COL = "urlid"; 90 private static final String FORMDATA_NAME_COL = "name"; 91 private static final String FORMDATA_VALUE_COL = "value"; 92 93 // column id strings for "httpauth" table 94 private static final String HTTPAUTH_HOST_COL = "host"; 95 private static final String HTTPAUTH_REALM_COL = "realm"; 96 private static final String HTTPAUTH_USERNAME_COL = "username"; 97 private static final String HTTPAUTH_PASSWORD_COL = "password"; 98 99 // Initially true until the background thread completes. 100 private boolean mInitialized = false; 101 102 WebViewDatabaseClassic(final Context context) { 103 new Thread() { 104 @Override 105 public void run() { 106 init(context); 107 } 108 }.start(); 109 110 // Singleton only, use getInstance() 111 } 112 113 public static synchronized WebViewDatabaseClassic getInstance(Context context) { 114 if (sInstance == null) { 115 sInstance = new WebViewDatabaseClassic(context); 116 } 117 return sInstance; 118 } 119 120 private synchronized void init(Context context) { 121 if (mInitialized) { 122 return; 123 } 124 125 initDatabase(context); 126 // Before using the Chromium HTTP stack, we stored the WebKit cache in 127 // our own DB. Clean up the DB file if it's still around. 128 context.deleteDatabase(CACHE_DATABASE_FILE); 129 130 // Thread done, notify. 131 mInitialized = true; 132 notify(); 133 } 134 135 private void initDatabase(Context context) { 136 try { 137 sDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null); 138 } catch (SQLiteException e) { 139 // try again by deleting the old db and create a new one 140 if (context.deleteDatabase(DATABASE_FILE)) { 141 sDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, 142 null); 143 } 144 } 145 146 // sDatabase should not be null, 147 // the only case is RequestAPI test has problem to create db 148 if (sDatabase == null) { 149 mInitialized = true; 150 notify(); 151 return; 152 } 153 154 if (sDatabase.getVersion() != DATABASE_VERSION) { 155 sDatabase.beginTransactionNonExclusive(); 156 try { 157 upgradeDatabase(); 158 sDatabase.setTransactionSuccessful(); 159 } finally { 160 sDatabase.endTransaction(); 161 } 162 } 163 } 164 165 private static void upgradeDatabase() { 166 upgradeDatabaseToV10(); 167 upgradeDatabaseFromV10ToV11(); 168 // Add future database upgrade functions here, one version at a 169 // time. 170 sDatabase.setVersion(DATABASE_VERSION); 171 } 172 173 private static void upgradeDatabaseFromV10ToV11() { 174 int oldVersion = sDatabase.getVersion(); 175 176 if (oldVersion >= 11) { 177 // Nothing to do. 178 return; 179 } 180 181 // Clear out old java stack cookies - this data is now stored in 182 // a separate database managed by the Chrome stack. 183 sDatabase.execSQL("DROP TABLE IF EXISTS cookies"); 184 185 // Likewise for the old cache table. 186 sDatabase.execSQL("DROP TABLE IF EXISTS cache"); 187 188 // Update form autocomplete URLs to match new ICS formatting. 189 Cursor c = sDatabase.query(mTableNames[TABLE_FORMURL_ID], null, null, 190 null, null, null, null); 191 while (c.moveToNext()) { 192 String urlId = Long.toString(c.getLong(c.getColumnIndex(ID_COL))); 193 String url = c.getString(c.getColumnIndex(FORMURL_URL_COL)); 194 ContentValues cv = new ContentValues(1); 195 cv.put(FORMURL_URL_COL, WebTextView.urlForAutoCompleteData(url)); 196 sDatabase.update(mTableNames[TABLE_FORMURL_ID], cv, ID_COL + "=?", 197 new String[] { urlId }); 198 } 199 c.close(); 200 } 201 202 private static void upgradeDatabaseToV10() { 203 int oldVersion = sDatabase.getVersion(); 204 205 if (oldVersion >= 10) { 206 // Nothing to do. 207 return; 208 } 209 210 if (oldVersion != 0) { 211 Log.i(LOGTAG, "Upgrading database from version " 212 + oldVersion + " to " 213 + DATABASE_VERSION + ", which will destroy old data"); 214 } 215 216 if (9 == oldVersion) { 217 sDatabase.execSQL("DROP TABLE IF EXISTS " 218 + mTableNames[TABLE_HTTPAUTH_ID]); 219 sDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID] 220 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 221 + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL 222 + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " 223 + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" 224 + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL 225 + ") ON CONFLICT REPLACE);"); 226 return; 227 } 228 229 sDatabase.execSQL("DROP TABLE IF EXISTS cookies"); 230 sDatabase.execSQL("DROP TABLE IF EXISTS cache"); 231 sDatabase.execSQL("DROP TABLE IF EXISTS " 232 + mTableNames[TABLE_FORMURL_ID]); 233 sDatabase.execSQL("DROP TABLE IF EXISTS " 234 + mTableNames[TABLE_FORMDATA_ID]); 235 sDatabase.execSQL("DROP TABLE IF EXISTS " 236 + mTableNames[TABLE_HTTPAUTH_ID]); 237 sDatabase.execSQL("DROP TABLE IF EXISTS " 238 + mTableNames[TABLE_PASSWORD_ID]); 239 240 // formurl 241 sDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMURL_ID] 242 + " (" + ID_COL + " INTEGER PRIMARY KEY, " + FORMURL_URL_COL 243 + " TEXT" + ");"); 244 245 // formdata 246 sDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMDATA_ID] 247 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 248 + FORMDATA_URLID_COL + " INTEGER, " + FORMDATA_NAME_COL 249 + " TEXT, " + FORMDATA_VALUE_COL + " TEXT," + " UNIQUE (" 250 + FORMDATA_URLID_COL + ", " + FORMDATA_NAME_COL + ", " 251 + FORMDATA_VALUE_COL + ") ON CONFLICT IGNORE);"); 252 253 // httpauth 254 sDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID] 255 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 256 + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL 257 + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " 258 + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" 259 + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL 260 + ") ON CONFLICT REPLACE);"); 261 // passwords 262 sDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID] 263 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 264 + PASSWORD_HOST_COL + " TEXT, " + PASSWORD_USERNAME_COL 265 + " TEXT, " + PASSWORD_PASSWORD_COL + " TEXT," + " UNIQUE (" 266 + PASSWORD_HOST_COL + ", " + PASSWORD_USERNAME_COL 267 + ") ON CONFLICT REPLACE);"); 268 } 269 270 // Wait for the background initialization thread to complete and check the 271 // database creation status. 272 private boolean checkInitialized() { 273 synchronized (this) { 274 while (!mInitialized) { 275 try { 276 wait(); 277 } catch (InterruptedException e) { 278 Log.e(LOGTAG, "Caught exception while checking " + 279 "initialization"); 280 Log.e(LOGTAG, Log.getStackTraceString(e)); 281 } 282 } 283 } 284 return sDatabase != null; 285 } 286 287 private boolean hasEntries(int tableId) { 288 if (!checkInitialized()) { 289 return false; 290 } 291 292 Cursor cursor = null; 293 boolean ret = false; 294 try { 295 cursor = sDatabase.query(mTableNames[tableId], ID_PROJECTION, 296 null, null, null, null, null); 297 ret = cursor.moveToFirst() == true; 298 } catch (IllegalStateException e) { 299 Log.e(LOGTAG, "hasEntries", e); 300 } finally { 301 if (cursor != null) cursor.close(); 302 } 303 return ret; 304 } 305 306 // 307 // password functions 308 // 309 310 /** 311 * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique. 312 * 313 * @param schemePlusHost The scheme and host for the password 314 * @param username The username for the password. If it is null, it means 315 * password can't be saved. 316 * @param password The password 317 */ 318 void setUsernamePassword(String schemePlusHost, String username, 319 String password) { 320 if (schemePlusHost == null || !checkInitialized()) { 321 return; 322 } 323 324 synchronized (mPasswordLock) { 325 final ContentValues c = new ContentValues(); 326 c.put(PASSWORD_HOST_COL, schemePlusHost); 327 c.put(PASSWORD_USERNAME_COL, username); 328 c.put(PASSWORD_PASSWORD_COL, password); 329 sDatabase.insert(mTableNames[TABLE_PASSWORD_ID], PASSWORD_HOST_COL, 330 c); 331 } 332 } 333 334 /** 335 * Retrieve the username and password for a given host 336 * 337 * @param schemePlusHost The scheme and host which passwords applies to 338 * @return String[] if found, String[0] is username, which can be null and 339 * String[1] is password. Return null if it can't find anything. 340 */ 341 String[] getUsernamePassword(String schemePlusHost) { 342 if (schemePlusHost == null || !checkInitialized()) { 343 return null; 344 } 345 346 final String[] columns = new String[] { 347 PASSWORD_USERNAME_COL, PASSWORD_PASSWORD_COL 348 }; 349 final String selection = "(" + PASSWORD_HOST_COL + " == ?)"; 350 synchronized (mPasswordLock) { 351 String[] ret = null; 352 Cursor cursor = null; 353 try { 354 cursor = sDatabase.query(mTableNames[TABLE_PASSWORD_ID], 355 columns, selection, new String[] { schemePlusHost }, null, 356 null, null); 357 if (cursor.moveToFirst()) { 358 ret = new String[2]; 359 ret[0] = cursor.getString( 360 cursor.getColumnIndex(PASSWORD_USERNAME_COL)); 361 ret[1] = cursor.getString( 362 cursor.getColumnIndex(PASSWORD_PASSWORD_COL)); 363 } 364 } catch (IllegalStateException e) { 365 Log.e(LOGTAG, "getUsernamePassword", e); 366 } finally { 367 if (cursor != null) cursor.close(); 368 } 369 return ret; 370 } 371 } 372 373 /** 374 * @see WebViewDatabase#hasUsernamePassword 375 */ 376 @Override 377 public boolean hasUsernamePassword() { 378 synchronized (mPasswordLock) { 379 return hasEntries(TABLE_PASSWORD_ID); 380 } 381 } 382 383 /** 384 * @see WebViewDatabase#clearUsernamePassword 385 */ 386 @Override 387 public void clearUsernamePassword() { 388 if (!checkInitialized()) { 389 return; 390 } 391 392 synchronized (mPasswordLock) { 393 sDatabase.delete(mTableNames[TABLE_PASSWORD_ID], null, null); 394 } 395 } 396 397 // 398 // http authentication password functions 399 // 400 401 /** 402 * Set HTTP authentication password. Tuple (HTTPAUTH_HOST_COL, 403 * HTTPAUTH_REALM_COL, HTTPAUTH_USERNAME_COL) is unique. 404 * 405 * @param host The host for the password 406 * @param realm The realm for the password 407 * @param username The username for the password. If it is null, it means 408 * password can't be saved. 409 * @param password The password 410 */ 411 void setHttpAuthUsernamePassword(String host, String realm, String username, 412 String password) { 413 if (host == null || realm == null || !checkInitialized()) { 414 return; 415 } 416 417 synchronized (mHttpAuthLock) { 418 final ContentValues c = new ContentValues(); 419 c.put(HTTPAUTH_HOST_COL, host); 420 c.put(HTTPAUTH_REALM_COL, realm); 421 c.put(HTTPAUTH_USERNAME_COL, username); 422 c.put(HTTPAUTH_PASSWORD_COL, password); 423 sDatabase.insert(mTableNames[TABLE_HTTPAUTH_ID], HTTPAUTH_HOST_COL, 424 c); 425 } 426 } 427 428 /** 429 * Retrieve the HTTP authentication username and password for a given 430 * host+realm pair 431 * 432 * @param host The host the password applies to 433 * @param realm The realm the password applies to 434 * @return String[] if found, String[0] is username, which can be null and 435 * String[1] is password. Return null if it can't find anything. 436 */ 437 String[] getHttpAuthUsernamePassword(String host, String realm) { 438 if (host == null || realm == null || !checkInitialized()){ 439 return null; 440 } 441 442 final String[] columns = new String[] { 443 HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL 444 }; 445 final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND (" 446 + HTTPAUTH_REALM_COL + " == ?)"; 447 synchronized (mHttpAuthLock) { 448 String[] ret = null; 449 Cursor cursor = null; 450 try { 451 cursor = sDatabase.query(mTableNames[TABLE_HTTPAUTH_ID], 452 columns, selection, new String[] { host, realm }, null, 453 null, null); 454 if (cursor.moveToFirst()) { 455 ret = new String[2]; 456 ret[0] = cursor.getString( 457 cursor.getColumnIndex(HTTPAUTH_USERNAME_COL)); 458 ret[1] = cursor.getString( 459 cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL)); 460 } 461 } catch (IllegalStateException e) { 462 Log.e(LOGTAG, "getHttpAuthUsernamePassword", e); 463 } finally { 464 if (cursor != null) cursor.close(); 465 } 466 return ret; 467 } 468 } 469 470 /** 471 * @see WebViewDatabase#hasHttpAuthUsernamePassword 472 */ 473 @Override 474 public boolean hasHttpAuthUsernamePassword() { 475 synchronized (mHttpAuthLock) { 476 return hasEntries(TABLE_HTTPAUTH_ID); 477 } 478 } 479 480 /** 481 * @see WebViewDatabase#clearHttpAuthUsernamePassword 482 */ 483 @Override 484 public void clearHttpAuthUsernamePassword() { 485 if (!checkInitialized()) { 486 return; 487 } 488 489 synchronized (mHttpAuthLock) { 490 sDatabase.delete(mTableNames[TABLE_HTTPAUTH_ID], null, null); 491 } 492 } 493 494 // 495 // form data functions 496 // 497 498 /** 499 * Set form data for a site. Tuple (FORMDATA_URLID_COL, FORMDATA_NAME_COL, 500 * FORMDATA_VALUE_COL) is unique 501 * 502 * @param url The url of the site 503 * @param formdata The form data in HashMap 504 */ 505 void setFormData(String url, HashMap<String, String> formdata) { 506 if (url == null || formdata == null || !checkInitialized()) { 507 return; 508 } 509 510 final String selection = "(" + FORMURL_URL_COL + " == ?)"; 511 synchronized (mFormLock) { 512 long urlid = -1; 513 Cursor cursor = null; 514 try { 515 cursor = sDatabase.query(mTableNames[TABLE_FORMURL_ID], 516 ID_PROJECTION, selection, new String[] { url }, null, null, 517 null); 518 if (cursor.moveToFirst()) { 519 urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); 520 } else { 521 ContentValues c = new ContentValues(); 522 c.put(FORMURL_URL_COL, url); 523 urlid = sDatabase.insert( 524 mTableNames[TABLE_FORMURL_ID], null, c); 525 } 526 } catch (IllegalStateException e) { 527 Log.e(LOGTAG, "setFormData", e); 528 } finally { 529 if (cursor != null) cursor.close(); 530 } 531 if (urlid >= 0) { 532 Set<Entry<String, String>> set = formdata.entrySet(); 533 Iterator<Entry<String, String>> iter = set.iterator(); 534 ContentValues map = new ContentValues(); 535 map.put(FORMDATA_URLID_COL, urlid); 536 while (iter.hasNext()) { 537 Entry<String, String> entry = iter.next(); 538 map.put(FORMDATA_NAME_COL, entry.getKey()); 539 map.put(FORMDATA_VALUE_COL, entry.getValue()); 540 sDatabase.insert(mTableNames[TABLE_FORMDATA_ID], null, map); 541 } 542 } 543 } 544 } 545 546 /** 547 * Get all the values for a form entry with "name" in a given site 548 * 549 * @param url The url of the site 550 * @param name The name of the form entry 551 * @return A list of values. Return empty list if nothing is found. 552 */ 553 ArrayList<String> getFormData(String url, String name) { 554 ArrayList<String> values = new ArrayList<String>(); 555 if (url == null || name == null || !checkInitialized()) { 556 return values; 557 } 558 559 final String urlSelection = "(" + FORMURL_URL_COL + " == ?)"; 560 final String dataSelection = "(" + FORMDATA_URLID_COL + " == ?) AND (" 561 + FORMDATA_NAME_COL + " == ?)"; 562 synchronized (mFormLock) { 563 Cursor cursor = null; 564 try { 565 cursor = sDatabase.query(mTableNames[TABLE_FORMURL_ID], 566 ID_PROJECTION, urlSelection, new String[] { url }, null, 567 null, null); 568 while (cursor.moveToNext()) { 569 long urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); 570 Cursor dataCursor = null; 571 try { 572 dataCursor = sDatabase.query( 573 mTableNames[TABLE_FORMDATA_ID], 574 new String[] { ID_COL, FORMDATA_VALUE_COL }, 575 dataSelection, 576 new String[] { Long.toString(urlid), name }, 577 null, null, null); 578 if (dataCursor.moveToFirst()) { 579 int valueCol = dataCursor.getColumnIndex( 580 FORMDATA_VALUE_COL); 581 do { 582 values.add(dataCursor.getString(valueCol)); 583 } while (dataCursor.moveToNext()); 584 } 585 } catch (IllegalStateException e) { 586 Log.e(LOGTAG, "getFormData dataCursor", e); 587 } finally { 588 if (dataCursor != null) dataCursor.close(); 589 } 590 } 591 } catch (IllegalStateException e) { 592 Log.e(LOGTAG, "getFormData cursor", e); 593 } finally { 594 if (cursor != null) cursor.close(); 595 } 596 return values; 597 } 598 } 599 600 /** 601 * @see WebViewDatabase#hasFormData 602 */ 603 @Override 604 public boolean hasFormData() { 605 synchronized (mFormLock) { 606 return hasEntries(TABLE_FORMURL_ID); 607 } 608 } 609 610 /** 611 * @see WebViewDatabase#clearFormData 612 */ 613 @Override 614 public void clearFormData() { 615 if (!checkInitialized()) { 616 return; 617 } 618 619 synchronized (mFormLock) { 620 sDatabase.delete(mTableNames[TABLE_FORMURL_ID], null, null); 621 sDatabase.delete(mTableNames[TABLE_FORMDATA_ID], null, null); 622 } 623 } 624 } 625