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