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