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 if (mCacheDatabase == null) { 731 return 0; 732 } 733 long size = 0; 734 Cursor cursor = null; 735 final String query = "SELECT SUM(contentlength) as sum FROM cache"; 736 try { 737 cursor = mCacheDatabase.rawQuery(query, null); 738 if (cursor.moveToFirst()) { 739 size = cursor.getLong(0); 740 } 741 } catch (IllegalStateException e) { 742 Log.e(LOGTAG, "getCacheTotalSize", e); 743 } finally { 744 if (cursor != null) cursor.close(); 745 } 746 return size; 747 } 748 749 List<String> trimCache(long amount) { 750 ArrayList<String> pathList = new ArrayList<String>(100); 751 Cursor cursor = null; 752 final String query = "SELECT contentlength, filepath FROM cache ORDER BY expires ASC"; 753 try { 754 cursor = mCacheDatabase.rawQuery(query, null); 755 if (cursor.moveToFirst()) { 756 int batchSize = 100; 757 StringBuilder pathStr = new StringBuilder(20 + 16 * batchSize); 758 pathStr.append("DELETE FROM cache WHERE filepath IN (?"); 759 for (int i = 1; i < batchSize; i++) { 760 pathStr.append(", ?"); 761 } 762 pathStr.append(")"); 763 SQLiteStatement statement = null; 764 try { 765 statement = mCacheDatabase.compileStatement( 766 pathStr.toString()); 767 // as bindString() uses 1-based index, initialize index to 1 768 int index = 1; 769 do { 770 long length = cursor.getLong(0); 771 if (length == 0) { 772 continue; 773 } 774 amount -= length; 775 String filePath = cursor.getString(1); 776 statement.bindString(index, filePath); 777 pathList.add(filePath); 778 if (index++ == batchSize) { 779 statement.execute(); 780 statement.clearBindings(); 781 index = 1; 782 } 783 } while (cursor.moveToNext() && amount > 0); 784 if (index > 1) { 785 // there may be old bindings from the previous statement 786 // if index is less than batchSize, which is Ok. 787 statement.execute(); 788 } 789 } catch (IllegalStateException e) { 790 Log.e(LOGTAG, "trimCache SQLiteStatement", e); 791 } finally { 792 if (statement != null) statement.close(); 793 } 794 } 795 } catch (IllegalStateException e) { 796 Log.e(LOGTAG, "trimCache Cursor", e); 797 } finally { 798 if (cursor != null) cursor.close(); 799 } 800 return pathList; 801 } 802 803 List<String> getAllCacheFileNames() { 804 ArrayList<String> pathList = null; 805 Cursor cursor = null; 806 try { 807 cursor = mCacheDatabase.rawQuery("SELECT filepath FROM cache", 808 null); 809 if (cursor != null && cursor.moveToFirst()) { 810 pathList = new ArrayList<String>(cursor.getCount()); 811 do { 812 pathList.add(cursor.getString(0)); 813 } while (cursor.moveToNext()); 814 } 815 } catch (IllegalStateException e) { 816 Log.e(LOGTAG, "getAllCacheFileNames", e); 817 } finally { 818 if (cursor != null) cursor.close(); 819 } 820 return pathList; 821 } 822 823 // 824 // password functions 825 // 826 827 /** 828 * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique. 829 * 830 * @param schemePlusHost The scheme and host for the password 831 * @param username The username for the password. If it is null, it means 832 * password can't be saved. 833 * @param password The password 834 */ 835 void setUsernamePassword(String schemePlusHost, String username, 836 String password) { 837 if (schemePlusHost == null || mDatabase == null) { 838 return; 839 } 840 841 synchronized (mPasswordLock) { 842 final ContentValues c = new ContentValues(); 843 c.put(PASSWORD_HOST_COL, schemePlusHost); 844 c.put(PASSWORD_USERNAME_COL, username); 845 c.put(PASSWORD_PASSWORD_COL, password); 846 mDatabase.insert(mTableNames[TABLE_PASSWORD_ID], PASSWORD_HOST_COL, 847 c); 848 } 849 } 850 851 /** 852 * Retrieve the username and password for a given host 853 * 854 * @param schemePlusHost The scheme and host which passwords applies to 855 * @return String[] if found, String[0] is username, which can be null and 856 * String[1] is password. Return null if it can't find anything. 857 */ 858 String[] getUsernamePassword(String schemePlusHost) { 859 if (schemePlusHost == null || mDatabase == null) { 860 return null; 861 } 862 863 final String[] columns = new String[] { 864 PASSWORD_USERNAME_COL, PASSWORD_PASSWORD_COL 865 }; 866 final String selection = "(" + PASSWORD_HOST_COL + " == ?)"; 867 synchronized (mPasswordLock) { 868 String[] ret = null; 869 Cursor cursor = null; 870 try { 871 cursor = mDatabase.query(mTableNames[TABLE_PASSWORD_ID], 872 columns, selection, new String[] { schemePlusHost }, null, 873 null, null); 874 if (cursor.moveToFirst()) { 875 ret = new String[2]; 876 ret[0] = cursor.getString( 877 cursor.getColumnIndex(PASSWORD_USERNAME_COL)); 878 ret[1] = cursor.getString( 879 cursor.getColumnIndex(PASSWORD_PASSWORD_COL)); 880 } 881 } catch (IllegalStateException e) { 882 Log.e(LOGTAG, "getUsernamePassword", e); 883 } finally { 884 if (cursor != null) cursor.close(); 885 } 886 return ret; 887 } 888 } 889 890 /** 891 * Find out if there are any passwords saved. 892 * 893 * @return TRUE if there is passwords saved 894 */ 895 public boolean hasUsernamePassword() { 896 synchronized (mPasswordLock) { 897 return hasEntries(TABLE_PASSWORD_ID); 898 } 899 } 900 901 /** 902 * Clear password database 903 */ 904 public void clearUsernamePassword() { 905 if (mDatabase == null) { 906 return; 907 } 908 909 synchronized (mPasswordLock) { 910 mDatabase.delete(mTableNames[TABLE_PASSWORD_ID], null, null); 911 } 912 } 913 914 // 915 // http authentication password functions 916 // 917 918 /** 919 * Set HTTP authentication password. Tuple (HTTPAUTH_HOST_COL, 920 * HTTPAUTH_REALM_COL, HTTPAUTH_USERNAME_COL) is unique. 921 * 922 * @param host The host for the password 923 * @param realm The realm for the password 924 * @param username The username for the password. If it is null, it means 925 * password can't be saved. 926 * @param password The password 927 */ 928 void setHttpAuthUsernamePassword(String host, String realm, String username, 929 String password) { 930 if (host == null || realm == null || mDatabase == null) { 931 return; 932 } 933 934 synchronized (mHttpAuthLock) { 935 final ContentValues c = new ContentValues(); 936 c.put(HTTPAUTH_HOST_COL, host); 937 c.put(HTTPAUTH_REALM_COL, realm); 938 c.put(HTTPAUTH_USERNAME_COL, username); 939 c.put(HTTPAUTH_PASSWORD_COL, password); 940 mDatabase.insert(mTableNames[TABLE_HTTPAUTH_ID], HTTPAUTH_HOST_COL, 941 c); 942 } 943 } 944 945 /** 946 * Retrieve the HTTP authentication username and password for a given 947 * host+realm pair 948 * 949 * @param host The host the password applies to 950 * @param realm The realm the password applies to 951 * @return String[] if found, String[0] is username, which can be null and 952 * String[1] is password. Return null if it can't find anything. 953 */ 954 String[] getHttpAuthUsernamePassword(String host, String realm) { 955 if (host == null || realm == null || mDatabase == null){ 956 return null; 957 } 958 959 final String[] columns = new String[] { 960 HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL 961 }; 962 final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND (" 963 + HTTPAUTH_REALM_COL + " == ?)"; 964 synchronized (mHttpAuthLock) { 965 String[] ret = null; 966 Cursor cursor = null; 967 try { 968 cursor = mDatabase.query(mTableNames[TABLE_HTTPAUTH_ID], 969 columns, selection, new String[] { host, realm }, null, 970 null, null); 971 if (cursor.moveToFirst()) { 972 ret = new String[2]; 973 ret[0] = cursor.getString( 974 cursor.getColumnIndex(HTTPAUTH_USERNAME_COL)); 975 ret[1] = cursor.getString( 976 cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL)); 977 } 978 } catch (IllegalStateException e) { 979 Log.e(LOGTAG, "getHttpAuthUsernamePassword", e); 980 } finally { 981 if (cursor != null) cursor.close(); 982 } 983 return ret; 984 } 985 } 986 987 /** 988 * Find out if there are any HTTP authentication passwords saved. 989 * 990 * @return TRUE if there are passwords saved 991 */ 992 public boolean hasHttpAuthUsernamePassword() { 993 synchronized (mHttpAuthLock) { 994 return hasEntries(TABLE_HTTPAUTH_ID); 995 } 996 } 997 998 /** 999 * Clear HTTP authentication password database 1000 */ 1001 public void clearHttpAuthUsernamePassword() { 1002 if (mDatabase == null) { 1003 return; 1004 } 1005 1006 synchronized (mHttpAuthLock) { 1007 mDatabase.delete(mTableNames[TABLE_HTTPAUTH_ID], null, null); 1008 } 1009 } 1010 1011 // 1012 // form data functions 1013 // 1014 1015 /** 1016 * Set form data for a site. Tuple (FORMDATA_URLID_COL, FORMDATA_NAME_COL, 1017 * FORMDATA_VALUE_COL) is unique 1018 * 1019 * @param url The url of the site 1020 * @param formdata The form data in HashMap 1021 */ 1022 void setFormData(String url, HashMap<String, String> formdata) { 1023 if (url == null || formdata == null || mDatabase == null) { 1024 return; 1025 } 1026 1027 final String selection = "(" + FORMURL_URL_COL + " == ?)"; 1028 synchronized (mFormLock) { 1029 long urlid = -1; 1030 Cursor cursor = null; 1031 try { 1032 cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID], 1033 ID_PROJECTION, selection, new String[] { url }, null, null, 1034 null); 1035 if (cursor.moveToFirst()) { 1036 urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); 1037 } else { 1038 ContentValues c = new ContentValues(); 1039 c.put(FORMURL_URL_COL, url); 1040 urlid = mDatabase.insert( 1041 mTableNames[TABLE_FORMURL_ID], null, c); 1042 } 1043 } catch (IllegalStateException e) { 1044 Log.e(LOGTAG, "setFormData", e); 1045 } finally { 1046 if (cursor != null) cursor.close(); 1047 } 1048 if (urlid >= 0) { 1049 Set<Entry<String, String>> set = formdata.entrySet(); 1050 Iterator<Entry<String, String>> iter = set.iterator(); 1051 ContentValues map = new ContentValues(); 1052 map.put(FORMDATA_URLID_COL, urlid); 1053 while (iter.hasNext()) { 1054 Entry<String, String> entry = iter.next(); 1055 map.put(FORMDATA_NAME_COL, entry.getKey()); 1056 map.put(FORMDATA_VALUE_COL, entry.getValue()); 1057 mDatabase.insert(mTableNames[TABLE_FORMDATA_ID], null, map); 1058 } 1059 } 1060 } 1061 } 1062 1063 /** 1064 * Get all the values for a form entry with "name" in a given site 1065 * 1066 * @param url The url of the site 1067 * @param name The name of the form entry 1068 * @return A list of values. Return empty list if nothing is found. 1069 */ 1070 ArrayList<String> getFormData(String url, String name) { 1071 ArrayList<String> values = new ArrayList<String>(); 1072 if (url == null || name == null || mDatabase == null) { 1073 return values; 1074 } 1075 1076 final String urlSelection = "(" + FORMURL_URL_COL + " == ?)"; 1077 final String dataSelection = "(" + FORMDATA_URLID_COL + " == ?) AND (" 1078 + FORMDATA_NAME_COL + " == ?)"; 1079 synchronized (mFormLock) { 1080 Cursor cursor = null; 1081 try { 1082 cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID], 1083 ID_PROJECTION, urlSelection, new String[] { url }, null, 1084 null, null); 1085 if (cursor.moveToFirst()) { 1086 long urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); 1087 Cursor dataCursor = null; 1088 try { 1089 dataCursor = mDatabase.query( 1090 mTableNames[TABLE_FORMDATA_ID], 1091 new String[] { ID_COL, FORMDATA_VALUE_COL }, 1092 dataSelection, 1093 new String[] { Long.toString(urlid), name }, 1094 null, null, null); 1095 if (dataCursor.moveToFirst()) { 1096 int valueCol = dataCursor.getColumnIndex( 1097 FORMDATA_VALUE_COL); 1098 do { 1099 values.add(dataCursor.getString(valueCol)); 1100 } while (dataCursor.moveToNext()); 1101 } 1102 } catch (IllegalStateException e) { 1103 Log.e(LOGTAG, "getFormData dataCursor", e); 1104 } finally { 1105 if (dataCursor != null) dataCursor.close(); 1106 } 1107 } 1108 } catch (IllegalStateException e) { 1109 Log.e(LOGTAG, "getFormData cursor", e); 1110 } finally { 1111 if (cursor != null) cursor.close(); 1112 } 1113 return values; 1114 } 1115 } 1116 1117 /** 1118 * Find out if there is form data saved. 1119 * 1120 * @return TRUE if there is form data in the database 1121 */ 1122 public boolean hasFormData() { 1123 synchronized (mFormLock) { 1124 return hasEntries(TABLE_FORMURL_ID); 1125 } 1126 } 1127 1128 /** 1129 * Clear form database 1130 */ 1131 public void clearFormData() { 1132 if (mDatabase == null) { 1133 return; 1134 } 1135 1136 synchronized (mFormLock) { 1137 mDatabase.delete(mTableNames[TABLE_FORMURL_ID], null, null); 1138 mDatabase.delete(mTableNames[TABLE_FORMDATA_ID], null, null); 1139 } 1140 } 1141 } 1142