1 /* 2 * Copyright (C) 2007 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 import android.webkit.CookieManager.Cookie; 35 import android.webkit.CacheManager.CacheResult; 36 import android.webkit.JniUtil; 37 38 public class WebViewDatabase { 39 private static final String DATABASE_FILE = "webview.db"; 40 private static final String CACHE_DATABASE_FILE = "webviewCache.db"; 41 42 // log tag 43 protected static final String LOGTAG = "webviewdatabase"; 44 45 private static final int DATABASE_VERSION = 11; 46 // 2 -> 3 Modified Cache table to allow cache of redirects 47 // 3 -> 4 Added Oma-Downloads table 48 // 4 -> 5 Modified Cache table to support persistent contentLength 49 // 5 -> 4 Removed Oma-Downoads table 50 // 5 -> 6 Add INDEX for cache table 51 // 6 -> 7 Change cache localPath from int to String 52 // 7 -> 8 Move cache to its own db 53 // 8 -> 9 Store both scheme and host when storing passwords 54 // 9 -> 10 Update httpauth table UNIQUE 55 // 10 -> 11 Drop cookies and cache now managed by the chromium stack, 56 // and update the form data table to use the new format 57 // implemented for b/5265606. 58 private static final int CACHE_DATABASE_VERSION = 4; 59 // 1 -> 2 Add expires String 60 // 2 -> 3 Add content-disposition 61 // 3 -> 4 Add crossdomain (For x-permitted-cross-domain-policies header) 62 63 private static WebViewDatabase mInstance = null; 64 65 private static SQLiteDatabase mDatabase = null; 66 private static SQLiteDatabase mCacheDatabase = null; 67 68 // synchronize locks 69 private final Object mCookieLock = new Object(); 70 private final Object mPasswordLock = new Object(); 71 private final Object mFormLock = new Object(); 72 private final Object mHttpAuthLock = new Object(); 73 74 // TODO: The Chromium HTTP stack handles cookies independently. 75 // We should consider removing the cookies table if and when we switch to 76 // the Chromium HTTP stack for good. 77 private static final String mTableNames[] = { 78 "cookies", "password", "formurl", "formdata", "httpauth" 79 }; 80 81 // Table ids (they are index to mTableNames) 82 private static final int TABLE_COOKIES_ID = 0; 83 84 private static final int TABLE_PASSWORD_ID = 1; 85 86 private static final int TABLE_FORMURL_ID = 2; 87 88 private static final int TABLE_FORMDATA_ID = 3; 89 90 private static final int TABLE_HTTPAUTH_ID = 4; 91 92 // column id strings for "_id" which can be used by any table 93 private static final String ID_COL = "_id"; 94 95 private static final String[] ID_PROJECTION = new String[] { 96 "_id" 97 }; 98 99 // column id strings for "cookies" table 100 private static final String COOKIES_NAME_COL = "name"; 101 102 private static final String COOKIES_VALUE_COL = "value"; 103 104 private static final String COOKIES_DOMAIN_COL = "domain"; 105 106 private static final String COOKIES_PATH_COL = "path"; 107 108 private static final String COOKIES_EXPIRES_COL = "expires"; 109 110 private static final String COOKIES_SECURE_COL = "secure"; 111 112 // column id strings for "cache" table 113 private static final String CACHE_URL_COL = "url"; 114 115 private static final String CACHE_FILE_PATH_COL = "filepath"; 116 117 private static final String CACHE_LAST_MODIFY_COL = "lastmodify"; 118 119 private static final String CACHE_ETAG_COL = "etag"; 120 121 private static final String CACHE_EXPIRES_COL = "expires"; 122 123 private static final String CACHE_EXPIRES_STRING_COL = "expiresstring"; 124 125 private static final String CACHE_MIMETYPE_COL = "mimetype"; 126 127 private static final String CACHE_ENCODING_COL = "encoding"; 128 129 private static final String CACHE_HTTP_STATUS_COL = "httpstatus"; 130 131 private static final String CACHE_LOCATION_COL = "location"; 132 133 private static final String CACHE_CONTENTLENGTH_COL = "contentlength"; 134 135 private static final String CACHE_CONTENTDISPOSITION_COL = "contentdisposition"; 136 137 private static final String CACHE_CROSSDOMAIN_COL = "crossdomain"; 138 139 // column id strings for "password" table 140 private static final String PASSWORD_HOST_COL = "host"; 141 142 private static final String PASSWORD_USERNAME_COL = "username"; 143 144 private static final String PASSWORD_PASSWORD_COL = "password"; 145 146 // column id strings for "formurl" table 147 private static final String FORMURL_URL_COL = "url"; 148 149 // column id strings for "formdata" table 150 private static final String FORMDATA_URLID_COL = "urlid"; 151 152 private static final String FORMDATA_NAME_COL = "name"; 153 154 private static final String FORMDATA_VALUE_COL = "value"; 155 156 // column id strings for "httpauth" table 157 private static final String HTTPAUTH_HOST_COL = "host"; 158 159 private static final String HTTPAUTH_REALM_COL = "realm"; 160 161 private static final String HTTPAUTH_USERNAME_COL = "username"; 162 163 private static final String HTTPAUTH_PASSWORD_COL = "password"; 164 165 // use InsertHelper to improve insert performance by 40% 166 private static DatabaseUtils.InsertHelper mCacheInserter; 167 private static int mCacheUrlColIndex; 168 private static int mCacheFilePathColIndex; 169 private static int mCacheLastModifyColIndex; 170 private static int mCacheETagColIndex; 171 private static int mCacheExpiresColIndex; 172 private static int mCacheExpiresStringColIndex; 173 private static int mCacheMimeTypeColIndex; 174 private static int mCacheEncodingColIndex; 175 private static int mCacheHttpStatusColIndex; 176 private static int mCacheLocationColIndex; 177 private static int mCacheContentLengthColIndex; 178 private static int mCacheContentDispositionColIndex; 179 private static int mCacheCrossDomainColIndex; 180 181 private static int mCacheTransactionRefcount; 182 183 // Initially true until the background thread completes. 184 private boolean mInitialized = false; 185 186 private WebViewDatabase(final Context context) { 187 new Thread() { 188 @Override 189 public void run() { 190 init(context); 191 } 192 }.start(); 193 194 // Singleton only, use getInstance() 195 } 196 197 public static synchronized WebViewDatabase getInstance(Context context) { 198 if (mInstance == null) { 199 mInstance = new WebViewDatabase(context); 200 } 201 return mInstance; 202 } 203 204 private synchronized void init(Context context) { 205 if (mInitialized) { 206 return; 207 } 208 209 initDatabase(context); 210 if (JniUtil.useChromiumHttpStack()) { 211 context.deleteDatabase(CACHE_DATABASE_FILE); 212 } else { 213 initCacheDatabase(context); 214 } 215 216 // Thread done, notify. 217 mInitialized = true; 218 notify(); 219 } 220 221 private void initDatabase(Context context) { 222 try { 223 mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null); 224 } catch (SQLiteException e) { 225 // try again by deleting the old db and create a new one 226 if (context.deleteDatabase(DATABASE_FILE)) { 227 mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, 228 null); 229 } 230 } 231 mDatabase.enableWriteAheadLogging(); 232 233 // mDatabase should not be null, 234 // the only case is RequestAPI test has problem to create db 235 if (mDatabase == null) { 236 mInitialized = true; 237 notify(); 238 return; 239 } 240 241 if (mDatabase.getVersion() != DATABASE_VERSION) { 242 mDatabase.beginTransactionNonExclusive(); 243 try { 244 upgradeDatabase(); 245 mDatabase.setTransactionSuccessful(); 246 } finally { 247 mDatabase.endTransaction(); 248 } 249 } 250 251 // use per table Mutex lock, turn off database lock, this 252 // improves performance as database's ReentrantLock is 253 // expansive 254 mDatabase.setLockingEnabled(false); 255 } 256 257 private void initCacheDatabase(Context context) { 258 assert !JniUtil.useChromiumHttpStack(); 259 260 try { 261 mCacheDatabase = context.openOrCreateDatabase( 262 CACHE_DATABASE_FILE, 0, null); 263 } catch (SQLiteException e) { 264 // try again by deleting the old db and create a new one 265 if (context.deleteDatabase(CACHE_DATABASE_FILE)) { 266 mCacheDatabase = context.openOrCreateDatabase( 267 CACHE_DATABASE_FILE, 0, null); 268 } 269 } 270 mCacheDatabase.enableWriteAheadLogging(); 271 272 // mCacheDatabase should not be null, 273 // the only case is RequestAPI test has problem to create db 274 if (mCacheDatabase == null) { 275 mInitialized = true; 276 notify(); 277 return; 278 } 279 280 if (mCacheDatabase.getVersion() != CACHE_DATABASE_VERSION) { 281 mCacheDatabase.beginTransactionNonExclusive(); 282 try { 283 upgradeCacheDatabase(); 284 bootstrapCacheDatabase(); 285 mCacheDatabase.setTransactionSuccessful(); 286 } finally { 287 mCacheDatabase.endTransaction(); 288 } 289 // Erase the files from the file system in the 290 // case that the database was updated and the 291 // there were existing cache content 292 CacheManager.removeAllCacheFiles(); 293 } 294 295 // use read_uncommitted to speed up READ 296 mCacheDatabase.execSQL("PRAGMA read_uncommitted = true;"); 297 // as only READ can be called in the 298 // non-WebViewWorkerThread, and read_uncommitted is used, 299 // we can turn off database lock to use transaction. 300 mCacheDatabase.setLockingEnabled(false); 301 302 // use InsertHelper for faster insertion 303 mCacheInserter = 304 new DatabaseUtils.InsertHelper(mCacheDatabase, 305 "cache"); 306 mCacheUrlColIndex = mCacheInserter 307 .getColumnIndex(CACHE_URL_COL); 308 mCacheFilePathColIndex = mCacheInserter 309 .getColumnIndex(CACHE_FILE_PATH_COL); 310 mCacheLastModifyColIndex = mCacheInserter 311 .getColumnIndex(CACHE_LAST_MODIFY_COL); 312 mCacheETagColIndex = mCacheInserter 313 .getColumnIndex(CACHE_ETAG_COL); 314 mCacheExpiresColIndex = mCacheInserter 315 .getColumnIndex(CACHE_EXPIRES_COL); 316 mCacheExpiresStringColIndex = mCacheInserter 317 .getColumnIndex(CACHE_EXPIRES_STRING_COL); 318 mCacheMimeTypeColIndex = mCacheInserter 319 .getColumnIndex(CACHE_MIMETYPE_COL); 320 mCacheEncodingColIndex = mCacheInserter 321 .getColumnIndex(CACHE_ENCODING_COL); 322 mCacheHttpStatusColIndex = mCacheInserter 323 .getColumnIndex(CACHE_HTTP_STATUS_COL); 324 mCacheLocationColIndex = mCacheInserter 325 .getColumnIndex(CACHE_LOCATION_COL); 326 mCacheContentLengthColIndex = mCacheInserter 327 .getColumnIndex(CACHE_CONTENTLENGTH_COL); 328 mCacheContentDispositionColIndex = mCacheInserter 329 .getColumnIndex(CACHE_CONTENTDISPOSITION_COL); 330 mCacheCrossDomainColIndex = mCacheInserter 331 .getColumnIndex(CACHE_CROSSDOMAIN_COL); 332 } 333 334 private static void upgradeDatabase() { 335 upgradeDatabaseToV10(); 336 upgradeDatabaseFromV10ToV11(); 337 // Add future database upgrade functions here, one version at a 338 // time. 339 mDatabase.setVersion(DATABASE_VERSION); 340 } 341 342 private static void upgradeDatabaseFromV10ToV11() { 343 int oldVersion = mDatabase.getVersion(); 344 345 if (oldVersion >= 11) { 346 // Nothing to do. 347 return; 348 } 349 350 if (JniUtil.useChromiumHttpStack()) { 351 // Clear out old java stack cookies - this data is now stored in 352 // a separate database managed by the Chrome stack. 353 mDatabase.execSQL("DROP TABLE IF EXISTS " + mTableNames[TABLE_COOKIES_ID]); 354 355 // Likewise for the old cache table. 356 mDatabase.execSQL("DROP TABLE IF EXISTS cache"); 357 } 358 359 // Update form autocomplete URLs to match new ICS formatting. 360 Cursor c = mDatabase.query(mTableNames[TABLE_FORMURL_ID], null, null, 361 null, null, null, null); 362 while (c.moveToNext()) { 363 String urlId = Long.toString(c.getLong(c.getColumnIndex(ID_COL))); 364 String url = c.getString(c.getColumnIndex(FORMURL_URL_COL)); 365 ContentValues cv = new ContentValues(1); 366 cv.put(FORMURL_URL_COL, WebTextView.urlForAutoCompleteData(url)); 367 mDatabase.update(mTableNames[TABLE_FORMURL_ID], cv, ID_COL + "=?", 368 new String[] { urlId }); 369 } 370 c.close(); 371 } 372 373 private static void upgradeDatabaseToV10() { 374 int oldVersion = mDatabase.getVersion(); 375 376 if (oldVersion >= 10) { 377 // Nothing to do. 378 return; 379 } 380 381 if (oldVersion != 0) { 382 Log.i(LOGTAG, "Upgrading database from version " 383 + oldVersion + " to " 384 + DATABASE_VERSION + ", which will destroy old data"); 385 } 386 387 if (9 == oldVersion) { 388 mDatabase.execSQL("DROP TABLE IF EXISTS " 389 + mTableNames[TABLE_HTTPAUTH_ID]); 390 mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID] 391 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 392 + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL 393 + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " 394 + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" 395 + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL 396 + ") ON CONFLICT REPLACE);"); 397 return; 398 } 399 400 mDatabase.execSQL("DROP TABLE IF EXISTS " 401 + mTableNames[TABLE_COOKIES_ID]); 402 mDatabase.execSQL("DROP TABLE IF EXISTS cache"); 403 mDatabase.execSQL("DROP TABLE IF EXISTS " 404 + mTableNames[TABLE_FORMURL_ID]); 405 mDatabase.execSQL("DROP TABLE IF EXISTS " 406 + mTableNames[TABLE_FORMDATA_ID]); 407 mDatabase.execSQL("DROP TABLE IF EXISTS " 408 + mTableNames[TABLE_HTTPAUTH_ID]); 409 mDatabase.execSQL("DROP TABLE IF EXISTS " 410 + mTableNames[TABLE_PASSWORD_ID]); 411 412 // cookies 413 mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_COOKIES_ID] 414 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 415 + COOKIES_NAME_COL + " TEXT, " + COOKIES_VALUE_COL 416 + " TEXT, " + COOKIES_DOMAIN_COL + " TEXT, " 417 + COOKIES_PATH_COL + " TEXT, " + COOKIES_EXPIRES_COL 418 + " INTEGER, " + COOKIES_SECURE_COL + " INTEGER" + ");"); 419 mDatabase.execSQL("CREATE INDEX cookiesIndex ON " 420 + mTableNames[TABLE_COOKIES_ID] + " (path)"); 421 422 // formurl 423 mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMURL_ID] 424 + " (" + ID_COL + " INTEGER PRIMARY KEY, " + FORMURL_URL_COL 425 + " TEXT" + ");"); 426 427 // formdata 428 mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMDATA_ID] 429 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 430 + FORMDATA_URLID_COL + " INTEGER, " + FORMDATA_NAME_COL 431 + " TEXT, " + FORMDATA_VALUE_COL + " TEXT," + " UNIQUE (" 432 + FORMDATA_URLID_COL + ", " + FORMDATA_NAME_COL + ", " 433 + FORMDATA_VALUE_COL + ") ON CONFLICT IGNORE);"); 434 435 // httpauth 436 mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID] 437 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 438 + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL 439 + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " 440 + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" 441 + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL 442 + ") ON CONFLICT REPLACE);"); 443 // passwords 444 mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID] 445 + " (" + ID_COL + " INTEGER PRIMARY KEY, " 446 + PASSWORD_HOST_COL + " TEXT, " + PASSWORD_USERNAME_COL 447 + " TEXT, " + PASSWORD_PASSWORD_COL + " TEXT," + " UNIQUE (" 448 + PASSWORD_HOST_COL + ", " + PASSWORD_USERNAME_COL 449 + ") ON CONFLICT REPLACE);"); 450 } 451 452 private static void upgradeCacheDatabase() { 453 int oldVersion = mCacheDatabase.getVersion(); 454 if (oldVersion != 0) { 455 Log.i(LOGTAG, "Upgrading cache database from version " 456 + oldVersion + " to " 457 + CACHE_DATABASE_VERSION + ", which will destroy all old data"); 458 } 459 mCacheDatabase.execSQL("DROP TABLE IF EXISTS cache"); 460 mCacheDatabase.setVersion(CACHE_DATABASE_VERSION); 461 } 462 463 private static void bootstrapCacheDatabase() { 464 if (mCacheDatabase != null) { 465 mCacheDatabase.execSQL("CREATE TABLE cache" 466 + " (" + ID_COL + " INTEGER PRIMARY KEY, " + CACHE_URL_COL 467 + " TEXT, " + CACHE_FILE_PATH_COL + " TEXT, " 468 + CACHE_LAST_MODIFY_COL + " TEXT, " + CACHE_ETAG_COL 469 + " TEXT, " + CACHE_EXPIRES_COL + " INTEGER, " 470 + CACHE_EXPIRES_STRING_COL + " TEXT, " 471 + CACHE_MIMETYPE_COL + " TEXT, " + CACHE_ENCODING_COL 472 + " TEXT," + CACHE_HTTP_STATUS_COL + " INTEGER, " 473 + CACHE_LOCATION_COL + " TEXT, " + CACHE_CONTENTLENGTH_COL 474 + " INTEGER, " + CACHE_CONTENTDISPOSITION_COL + " TEXT, " 475 + CACHE_CROSSDOMAIN_COL + " TEXT," 476 + " UNIQUE (" + CACHE_URL_COL + ") ON CONFLICT REPLACE);"); 477 mCacheDatabase.execSQL("CREATE INDEX cacheUrlIndex ON cache (" 478 + CACHE_URL_COL + ")"); 479 } 480 } 481 482 // Wait for the background initialization thread to complete and check the 483 // database creation status. 484 private boolean checkInitialized() { 485 synchronized (this) { 486 while (!mInitialized) { 487 try { 488 wait(); 489 } catch (InterruptedException e) { 490 Log.e(LOGTAG, "Caught exception while checking " + 491 "initialization"); 492 Log.e(LOGTAG, Log.getStackTraceString(e)); 493 } 494 } 495 } 496 return mDatabase != null; 497 } 498 499 private boolean hasEntries(int tableId) { 500 if (!checkInitialized()) { 501 return false; 502 } 503 504 Cursor cursor = null; 505 boolean ret = false; 506 try { 507 cursor = mDatabase.query(mTableNames[tableId], ID_PROJECTION, 508 null, null, null, null, null); 509 ret = cursor.moveToFirst() == true; 510 } catch (IllegalStateException e) { 511 Log.e(LOGTAG, "hasEntries", e); 512 } finally { 513 if (cursor != null) cursor.close(); 514 } 515 return ret; 516 } 517 518 // 519 // cookies functions 520 // 521 522 /** 523 * Get cookies in the format of CookieManager.Cookie inside an ArrayList for 524 * a given domain 525 * 526 * @return ArrayList<Cookie> If nothing is found, return an empty list. 527 */ 528 ArrayList<Cookie> getCookiesForDomain(String domain) { 529 ArrayList<Cookie> list = new ArrayList<Cookie>(); 530 if (domain == null || !checkInitialized()) { 531 return list; 532 } 533 534 synchronized (mCookieLock) { 535 final String[] columns = new String[] { 536 ID_COL, COOKIES_DOMAIN_COL, COOKIES_PATH_COL, 537 COOKIES_NAME_COL, COOKIES_VALUE_COL, COOKIES_EXPIRES_COL, 538 COOKIES_SECURE_COL 539 }; 540 final String selection = "(" + COOKIES_DOMAIN_COL 541 + " GLOB '*' || ?)"; 542 Cursor cursor = null; 543 try { 544 cursor = mDatabase.query(mTableNames[TABLE_COOKIES_ID], 545 columns, selection, new String[] { domain }, null, null, 546 null); 547 if (cursor.moveToFirst()) { 548 int domainCol = cursor.getColumnIndex(COOKIES_DOMAIN_COL); 549 int pathCol = cursor.getColumnIndex(COOKIES_PATH_COL); 550 int nameCol = cursor.getColumnIndex(COOKIES_NAME_COL); 551 int valueCol = cursor.getColumnIndex(COOKIES_VALUE_COL); 552 int expiresCol = cursor.getColumnIndex(COOKIES_EXPIRES_COL); 553 int secureCol = cursor.getColumnIndex(COOKIES_SECURE_COL); 554 do { 555 Cookie cookie = new Cookie(); 556 cookie.domain = cursor.getString(domainCol); 557 cookie.path = cursor.getString(pathCol); 558 cookie.name = cursor.getString(nameCol); 559 cookie.value = cursor.getString(valueCol); 560 if (cursor.isNull(expiresCol)) { 561 cookie.expires = -1; 562 } else { 563 cookie.expires = cursor.getLong(expiresCol); 564 } 565 cookie.secure = cursor.getShort(secureCol) != 0; 566 cookie.mode = Cookie.MODE_NORMAL; 567 list.add(cookie); 568 } while (cursor.moveToNext()); 569 } 570 } catch (IllegalStateException e) { 571 Log.e(LOGTAG, "getCookiesForDomain", e); 572 } finally { 573 if (cursor != null) cursor.close(); 574 } 575 return list; 576 } 577 } 578 579 /** 580 * Delete cookies which matches (domain, path, name). 581 * 582 * @param domain If it is null, nothing happens. 583 * @param path If it is null, all the cookies match (domain) will be 584 * deleted. 585 * @param name If it is null, all the cookies match (domain, path) will be 586 * deleted. 587 */ 588 void deleteCookies(String domain, String path, String name) { 589 if (domain == null || !checkInitialized()) { 590 return; 591 } 592 593 synchronized (mCookieLock) { 594 final String where = "(" + COOKIES_DOMAIN_COL + " == ?) AND (" 595 + COOKIES_PATH_COL + " == ?) AND (" + COOKIES_NAME_COL 596 + " == ?)"; 597 mDatabase.delete(mTableNames[TABLE_COOKIES_ID], where, 598 new String[] { domain, path, name }); 599 } 600 } 601 602 /** 603 * Add a cookie to the database 604 * 605 * @param cookie 606 */ 607 void addCookie(Cookie cookie) { 608 if (cookie.domain == null || cookie.path == null || cookie.name == null 609 || !checkInitialized()) { 610 return; 611 } 612 613 synchronized (mCookieLock) { 614 ContentValues cookieVal = new ContentValues(); 615 cookieVal.put(COOKIES_DOMAIN_COL, cookie.domain); 616 cookieVal.put(COOKIES_PATH_COL, cookie.path); 617 cookieVal.put(COOKIES_NAME_COL, cookie.name); 618 cookieVal.put(COOKIES_VALUE_COL, cookie.value); 619 if (cookie.expires != -1) { 620 cookieVal.put(COOKIES_EXPIRES_COL, cookie.expires); 621 } 622 cookieVal.put(COOKIES_SECURE_COL, cookie.secure); 623 mDatabase.insert(mTableNames[TABLE_COOKIES_ID], null, cookieVal); 624 } 625 } 626 627 /** 628 * Whether there is any cookies in the database 629 * 630 * @return TRUE if there is cookie. 631 */ 632 boolean hasCookies() { 633 synchronized (mCookieLock) { 634 return hasEntries(TABLE_COOKIES_ID); 635 } 636 } 637 638 /** 639 * Clear cookie database 640 */ 641 void clearCookies() { 642 if (!checkInitialized()) { 643 return; 644 } 645 646 synchronized (mCookieLock) { 647 mDatabase.delete(mTableNames[TABLE_COOKIES_ID], null, null); 648 } 649 } 650 651 /** 652 * Clear session cookies, which means cookie doesn't have EXPIRES. 653 */ 654 void clearSessionCookies() { 655 if (!checkInitialized()) { 656 return; 657 } 658 659 final String sessionExpired = COOKIES_EXPIRES_COL + " ISNULL"; 660 synchronized (mCookieLock) { 661 mDatabase.delete(mTableNames[TABLE_COOKIES_ID], sessionExpired, 662 null); 663 } 664 } 665 666 /** 667 * Clear expired cookies 668 * 669 * @param now Time for now 670 */ 671 void clearExpiredCookies(long now) { 672 if (!checkInitialized()) { 673 return; 674 } 675 676 final String expires = COOKIES_EXPIRES_COL + " <= ?"; 677 synchronized (mCookieLock) { 678 mDatabase.delete(mTableNames[TABLE_COOKIES_ID], expires, 679 new String[] { Long.toString(now) }); 680 } 681 } 682 683 // 684 // cache functions 685 // 686 687 // only called from WebViewWorkerThread 688 boolean startCacheTransaction() { 689 if (++mCacheTransactionRefcount == 1) { 690 if (!Thread.currentThread().equals( 691 WebViewWorker.getHandler().getLooper().getThread())) { 692 Log.w(LOGTAG, "startCacheTransaction should be called from " 693 + "WebViewWorkerThread instead of from " 694 + Thread.currentThread().getName()); 695 } 696 mCacheDatabase.beginTransactionNonExclusive(); 697 return true; 698 } 699 return false; 700 } 701 702 // only called from WebViewWorkerThread 703 boolean endCacheTransaction() { 704 if (--mCacheTransactionRefcount == 0) { 705 if (!Thread.currentThread().equals( 706 WebViewWorker.getHandler().getLooper().getThread())) { 707 Log.w(LOGTAG, "endCacheTransaction should be called from " 708 + "WebViewWorkerThread instead of from " 709 + Thread.currentThread().getName()); 710 } 711 try { 712 mCacheDatabase.setTransactionSuccessful(); 713 } finally { 714 mCacheDatabase.endTransaction(); 715 } 716 return true; 717 } 718 return false; 719 } 720 721 /** 722 * Get a cache item. 723 * 724 * @param url The url 725 * @return CacheResult The CacheManager.CacheResult 726 */ 727 CacheResult getCache(String url) { 728 assert !JniUtil.useChromiumHttpStack(); 729 730 if (url == null || !checkInitialized()) { 731 return null; 732 } 733 734 Cursor cursor = null; 735 final String query = "SELECT filepath, lastmodify, etag, expires, " 736 + "expiresstring, mimetype, encoding, httpstatus, location, contentlength, " 737 + "contentdisposition, crossdomain FROM cache WHERE url = ?"; 738 try { 739 cursor = mCacheDatabase.rawQuery(query, new String[] { url }); 740 if (cursor.moveToFirst()) { 741 CacheResult ret = new CacheResult(); 742 ret.localPath = cursor.getString(0); 743 ret.lastModified = cursor.getString(1); 744 ret.etag = cursor.getString(2); 745 ret.expires = cursor.getLong(3); 746 ret.expiresString = cursor.getString(4); 747 ret.mimeType = cursor.getString(5); 748 ret.encoding = cursor.getString(6); 749 ret.httpStatusCode = cursor.getInt(7); 750 ret.location = cursor.getString(8); 751 ret.contentLength = cursor.getLong(9); 752 ret.contentdisposition = cursor.getString(10); 753 ret.crossDomain = cursor.getString(11); 754 return ret; 755 } 756 } catch (IllegalStateException e) { 757 Log.e(LOGTAG, "getCache", e); 758 } finally { 759 if (cursor != null) cursor.close(); 760 } 761 return null; 762 } 763 764 /** 765 * Remove a cache item. 766 * 767 * @param url The url 768 */ 769 void removeCache(String url) { 770 assert !JniUtil.useChromiumHttpStack(); 771 772 if (url == null || !checkInitialized()) { 773 return; 774 } 775 776 mCacheDatabase.execSQL("DELETE FROM cache WHERE url = ?", new String[] { url }); 777 } 778 779 /** 780 * Add or update a cache. CACHE_URL_COL is unique in the table. 781 * 782 * @param url The url 783 * @param c The CacheManager.CacheResult 784 */ 785 void addCache(String url, CacheResult c) { 786 assert !JniUtil.useChromiumHttpStack(); 787 788 if (url == null || !checkInitialized()) { 789 return; 790 } 791 792 mCacheInserter.prepareForInsert(); 793 mCacheInserter.bind(mCacheUrlColIndex, url); 794 mCacheInserter.bind(mCacheFilePathColIndex, c.localPath); 795 mCacheInserter.bind(mCacheLastModifyColIndex, c.lastModified); 796 mCacheInserter.bind(mCacheETagColIndex, c.etag); 797 mCacheInserter.bind(mCacheExpiresColIndex, c.expires); 798 mCacheInserter.bind(mCacheExpiresStringColIndex, c.expiresString); 799 mCacheInserter.bind(mCacheMimeTypeColIndex, c.mimeType); 800 mCacheInserter.bind(mCacheEncodingColIndex, c.encoding); 801 mCacheInserter.bind(mCacheHttpStatusColIndex, c.httpStatusCode); 802 mCacheInserter.bind(mCacheLocationColIndex, c.location); 803 mCacheInserter.bind(mCacheContentLengthColIndex, c.contentLength); 804 mCacheInserter.bind(mCacheContentDispositionColIndex, 805 c.contentdisposition); 806 mCacheInserter.bind(mCacheCrossDomainColIndex, c.crossDomain); 807 mCacheInserter.execute(); 808 } 809 810 /** 811 * Clear cache database 812 */ 813 void clearCache() { 814 if (!checkInitialized()) { 815 return; 816 } 817 818 mCacheDatabase.delete("cache", null, null); 819 } 820 821 boolean hasCache() { 822 if (!checkInitialized()) { 823 return false; 824 } 825 826 Cursor cursor = null; 827 boolean ret = false; 828 try { 829 cursor = mCacheDatabase.query("cache", ID_PROJECTION, 830 null, null, null, null, null); 831 ret = cursor.moveToFirst() == true; 832 } catch (IllegalStateException e) { 833 Log.e(LOGTAG, "hasCache", e); 834 } finally { 835 if (cursor != null) cursor.close(); 836 } 837 return ret; 838 } 839 840 long getCacheTotalSize() { 841 if (mCacheDatabase == null) { 842 return 0; 843 } 844 long size = 0; 845 Cursor cursor = null; 846 final String query = "SELECT SUM(contentlength) as sum FROM cache"; 847 try { 848 cursor = mCacheDatabase.rawQuery(query, null); 849 if (cursor.moveToFirst()) { 850 size = cursor.getLong(0); 851 } 852 } catch (IllegalStateException e) { 853 Log.e(LOGTAG, "getCacheTotalSize", e); 854 } finally { 855 if (cursor != null) cursor.close(); 856 } 857 return size; 858 } 859 860 List<String> trimCache(long amount) { 861 ArrayList<String> pathList = new ArrayList<String>(100); 862 Cursor cursor = null; 863 final String query = "SELECT contentlength, filepath FROM cache ORDER BY expires ASC"; 864 try { 865 cursor = mCacheDatabase.rawQuery(query, null); 866 if (cursor.moveToFirst()) { 867 int batchSize = 100; 868 StringBuilder pathStr = new StringBuilder(20 + 16 * batchSize); 869 pathStr.append("DELETE FROM cache WHERE filepath IN (?"); 870 for (int i = 1; i < batchSize; i++) { 871 pathStr.append(", ?"); 872 } 873 pathStr.append(")"); 874 SQLiteStatement statement = null; 875 try { 876 statement = mCacheDatabase.compileStatement( 877 pathStr.toString()); 878 // as bindString() uses 1-based index, initialize index to 1 879 int index = 1; 880 do { 881 long length = cursor.getLong(0); 882 if (length == 0) { 883 continue; 884 } 885 amount -= length; 886 String filePath = cursor.getString(1); 887 statement.bindString(index, filePath); 888 pathList.add(filePath); 889 if (index++ == batchSize) { 890 statement.execute(); 891 statement.clearBindings(); 892 index = 1; 893 } 894 } while (cursor.moveToNext() && amount > 0); 895 if (index > 1) { 896 // there may be old bindings from the previous statement 897 // if index is less than batchSize, which is Ok. 898 statement.execute(); 899 } 900 } catch (IllegalStateException e) { 901 Log.e(LOGTAG, "trimCache SQLiteStatement", e); 902 } finally { 903 if (statement != null) statement.close(); 904 } 905 } 906 } catch (IllegalStateException e) { 907 Log.e(LOGTAG, "trimCache Cursor", e); 908 } finally { 909 if (cursor != null) cursor.close(); 910 } 911 return pathList; 912 } 913 914 List<String> getAllCacheFileNames() { 915 ArrayList<String> pathList = null; 916 Cursor cursor = null; 917 try { 918 cursor = mCacheDatabase.rawQuery("SELECT filepath FROM cache", 919 null); 920 if (cursor != null && cursor.moveToFirst()) { 921 pathList = new ArrayList<String>(cursor.getCount()); 922 do { 923 pathList.add(cursor.getString(0)); 924 } while (cursor.moveToNext()); 925 } 926 } catch (IllegalStateException e) { 927 Log.e(LOGTAG, "getAllCacheFileNames", e); 928 } finally { 929 if (cursor != null) cursor.close(); 930 } 931 return pathList; 932 } 933 934 // 935 // password functions 936 // 937 938 /** 939 * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique. 940 * 941 * @param schemePlusHost The scheme and host for the password 942 * @param username The username for the password. If it is null, it means 943 * password can't be saved. 944 * @param password The password 945 */ 946 void setUsernamePassword(String schemePlusHost, String username, 947 String password) { 948 if (schemePlusHost == null || !checkInitialized()) { 949 return; 950 } 951 952 synchronized (mPasswordLock) { 953 final ContentValues c = new ContentValues(); 954 c.put(PASSWORD_HOST_COL, schemePlusHost); 955 c.put(PASSWORD_USERNAME_COL, username); 956 c.put(PASSWORD_PASSWORD_COL, password); 957 mDatabase.insert(mTableNames[TABLE_PASSWORD_ID], PASSWORD_HOST_COL, 958 c); 959 } 960 } 961 962 /** 963 * Retrieve the username and password for a given host 964 * 965 * @param schemePlusHost The scheme and host which passwords applies to 966 * @return String[] if found, String[0] is username, which can be null and 967 * String[1] is password. Return null if it can't find anything. 968 */ 969 String[] getUsernamePassword(String schemePlusHost) { 970 if (schemePlusHost == null || !checkInitialized()) { 971 return null; 972 } 973 974 final String[] columns = new String[] { 975 PASSWORD_USERNAME_COL, PASSWORD_PASSWORD_COL 976 }; 977 final String selection = "(" + PASSWORD_HOST_COL + " == ?)"; 978 synchronized (mPasswordLock) { 979 String[] ret = null; 980 Cursor cursor = null; 981 try { 982 cursor = mDatabase.query(mTableNames[TABLE_PASSWORD_ID], 983 columns, selection, new String[] { schemePlusHost }, null, 984 null, null); 985 if (cursor.moveToFirst()) { 986 ret = new String[2]; 987 ret[0] = cursor.getString( 988 cursor.getColumnIndex(PASSWORD_USERNAME_COL)); 989 ret[1] = cursor.getString( 990 cursor.getColumnIndex(PASSWORD_PASSWORD_COL)); 991 } 992 } catch (IllegalStateException e) { 993 Log.e(LOGTAG, "getUsernamePassword", e); 994 } finally { 995 if (cursor != null) cursor.close(); 996 } 997 return ret; 998 } 999 } 1000 1001 /** 1002 * Find out if there are any passwords saved. 1003 * 1004 * @return TRUE if there is passwords saved 1005 */ 1006 public boolean hasUsernamePassword() { 1007 synchronized (mPasswordLock) { 1008 return hasEntries(TABLE_PASSWORD_ID); 1009 } 1010 } 1011 1012 /** 1013 * Clear password database 1014 */ 1015 public void clearUsernamePassword() { 1016 if (!checkInitialized()) { 1017 return; 1018 } 1019 1020 synchronized (mPasswordLock) { 1021 mDatabase.delete(mTableNames[TABLE_PASSWORD_ID], null, null); 1022 } 1023 } 1024 1025 // 1026 // http authentication password functions 1027 // 1028 1029 /** 1030 * Set HTTP authentication password. Tuple (HTTPAUTH_HOST_COL, 1031 * HTTPAUTH_REALM_COL, HTTPAUTH_USERNAME_COL) is unique. 1032 * 1033 * @param host The host for the password 1034 * @param realm The realm for the password 1035 * @param username The username for the password. If it is null, it means 1036 * password can't be saved. 1037 * @param password The password 1038 */ 1039 void setHttpAuthUsernamePassword(String host, String realm, String username, 1040 String password) { 1041 if (host == null || realm == null || !checkInitialized()) { 1042 return; 1043 } 1044 1045 synchronized (mHttpAuthLock) { 1046 final ContentValues c = new ContentValues(); 1047 c.put(HTTPAUTH_HOST_COL, host); 1048 c.put(HTTPAUTH_REALM_COL, realm); 1049 c.put(HTTPAUTH_USERNAME_COL, username); 1050 c.put(HTTPAUTH_PASSWORD_COL, password); 1051 mDatabase.insert(mTableNames[TABLE_HTTPAUTH_ID], HTTPAUTH_HOST_COL, 1052 c); 1053 } 1054 } 1055 1056 /** 1057 * Retrieve the HTTP authentication username and password for a given 1058 * host+realm pair 1059 * 1060 * @param host The host the password applies to 1061 * @param realm The realm the password applies to 1062 * @return String[] if found, String[0] is username, which can be null and 1063 * String[1] is password. Return null if it can't find anything. 1064 */ 1065 String[] getHttpAuthUsernamePassword(String host, String realm) { 1066 if (host == null || realm == null || !checkInitialized()){ 1067 return null; 1068 } 1069 1070 final String[] columns = new String[] { 1071 HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL 1072 }; 1073 final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND (" 1074 + HTTPAUTH_REALM_COL + " == ?)"; 1075 synchronized (mHttpAuthLock) { 1076 String[] ret = null; 1077 Cursor cursor = null; 1078 try { 1079 cursor = mDatabase.query(mTableNames[TABLE_HTTPAUTH_ID], 1080 columns, selection, new String[] { host, realm }, null, 1081 null, null); 1082 if (cursor.moveToFirst()) { 1083 ret = new String[2]; 1084 ret[0] = cursor.getString( 1085 cursor.getColumnIndex(HTTPAUTH_USERNAME_COL)); 1086 ret[1] = cursor.getString( 1087 cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL)); 1088 } 1089 } catch (IllegalStateException e) { 1090 Log.e(LOGTAG, "getHttpAuthUsernamePassword", e); 1091 } finally { 1092 if (cursor != null) cursor.close(); 1093 } 1094 return ret; 1095 } 1096 } 1097 1098 /** 1099 * Find out if there are any HTTP authentication passwords saved. 1100 * 1101 * @return TRUE if there are passwords saved 1102 */ 1103 public boolean hasHttpAuthUsernamePassword() { 1104 synchronized (mHttpAuthLock) { 1105 return hasEntries(TABLE_HTTPAUTH_ID); 1106 } 1107 } 1108 1109 /** 1110 * Clear HTTP authentication password database 1111 */ 1112 public void clearHttpAuthUsernamePassword() { 1113 if (!checkInitialized()) { 1114 return; 1115 } 1116 1117 synchronized (mHttpAuthLock) { 1118 mDatabase.delete(mTableNames[TABLE_HTTPAUTH_ID], null, null); 1119 } 1120 } 1121 1122 // 1123 // form data functions 1124 // 1125 1126 /** 1127 * Set form data for a site. Tuple (FORMDATA_URLID_COL, FORMDATA_NAME_COL, 1128 * FORMDATA_VALUE_COL) is unique 1129 * 1130 * @param url The url of the site 1131 * @param formdata The form data in HashMap 1132 */ 1133 void setFormData(String url, HashMap<String, String> formdata) { 1134 if (url == null || formdata == null || !checkInitialized()) { 1135 return; 1136 } 1137 1138 final String selection = "(" + FORMURL_URL_COL + " == ?)"; 1139 synchronized (mFormLock) { 1140 long urlid = -1; 1141 Cursor cursor = null; 1142 try { 1143 cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID], 1144 ID_PROJECTION, selection, new String[] { url }, null, null, 1145 null); 1146 if (cursor.moveToFirst()) { 1147 urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); 1148 } else { 1149 ContentValues c = new ContentValues(); 1150 c.put(FORMURL_URL_COL, url); 1151 urlid = mDatabase.insert( 1152 mTableNames[TABLE_FORMURL_ID], null, c); 1153 } 1154 } catch (IllegalStateException e) { 1155 Log.e(LOGTAG, "setFormData", e); 1156 } finally { 1157 if (cursor != null) cursor.close(); 1158 } 1159 if (urlid >= 0) { 1160 Set<Entry<String, String>> set = formdata.entrySet(); 1161 Iterator<Entry<String, String>> iter = set.iterator(); 1162 ContentValues map = new ContentValues(); 1163 map.put(FORMDATA_URLID_COL, urlid); 1164 while (iter.hasNext()) { 1165 Entry<String, String> entry = iter.next(); 1166 map.put(FORMDATA_NAME_COL, entry.getKey()); 1167 map.put(FORMDATA_VALUE_COL, entry.getValue()); 1168 mDatabase.insert(mTableNames[TABLE_FORMDATA_ID], null, map); 1169 } 1170 } 1171 } 1172 } 1173 1174 /** 1175 * Get all the values for a form entry with "name" in a given site 1176 * 1177 * @param url The url of the site 1178 * @param name The name of the form entry 1179 * @return A list of values. Return empty list if nothing is found. 1180 */ 1181 ArrayList<String> getFormData(String url, String name) { 1182 ArrayList<String> values = new ArrayList<String>(); 1183 if (url == null || name == null || !checkInitialized()) { 1184 return values; 1185 } 1186 1187 final String urlSelection = "(" + FORMURL_URL_COL + " == ?)"; 1188 final String dataSelection = "(" + FORMDATA_URLID_COL + " == ?) AND (" 1189 + FORMDATA_NAME_COL + " == ?)"; 1190 synchronized (mFormLock) { 1191 Cursor cursor = null; 1192 try { 1193 cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID], 1194 ID_PROJECTION, urlSelection, new String[] { url }, null, 1195 null, null); 1196 while (cursor.moveToNext()) { 1197 long urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); 1198 Cursor dataCursor = null; 1199 try { 1200 dataCursor = mDatabase.query( 1201 mTableNames[TABLE_FORMDATA_ID], 1202 new String[] { ID_COL, FORMDATA_VALUE_COL }, 1203 dataSelection, 1204 new String[] { Long.toString(urlid), name }, 1205 null, null, null); 1206 if (dataCursor.moveToFirst()) { 1207 int valueCol = dataCursor.getColumnIndex( 1208 FORMDATA_VALUE_COL); 1209 do { 1210 values.add(dataCursor.getString(valueCol)); 1211 } while (dataCursor.moveToNext()); 1212 } 1213 } catch (IllegalStateException e) { 1214 Log.e(LOGTAG, "getFormData dataCursor", e); 1215 } finally { 1216 if (dataCursor != null) dataCursor.close(); 1217 } 1218 } 1219 } catch (IllegalStateException e) { 1220 Log.e(LOGTAG, "getFormData cursor", e); 1221 } finally { 1222 if (cursor != null) cursor.close(); 1223 } 1224 return values; 1225 } 1226 } 1227 1228 /** 1229 * Find out if there is form data saved. 1230 * 1231 * @return TRUE if there is form data in the database 1232 */ 1233 public boolean hasFormData() { 1234 synchronized (mFormLock) { 1235 return hasEntries(TABLE_FORMURL_ID); 1236 } 1237 } 1238 1239 /** 1240 * Clear form database 1241 */ 1242 public void clearFormData() { 1243 if (!checkInitialized()) { 1244 return; 1245 } 1246 1247 synchronized (mFormLock) { 1248 mDatabase.delete(mTableNames[TABLE_FORMURL_ID], null, null); 1249 mDatabase.delete(mTableNames[TABLE_FORMDATA_ID], null, null); 1250 } 1251 } 1252 } 1253