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