• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
WebViewDatabase()176     private WebViewDatabase() {
177         // Singleton only, use getInstance()
178     }
179 
getInstance(Context context)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 
upgradeDatabase()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 
upgradeCacheDatabase()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 
bootstrapCacheDatabase()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 
hasEntries(int tableId)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      */
getCookiesForDomain(String domain)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      */
deleteCookies(String domain, String path, String name)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      */
addCookie(Cookie cookie)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      */
hasCookies()527     boolean hasCookies() {
528         synchronized (mCookieLock) {
529             return hasEntries(TABLE_COOKIES_ID);
530         }
531     }
532 
533     /**
534      * Clear cookie database
535      */
clearCookies()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      */
clearSessionCookies()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      */
clearExpiredCookies(long now)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
startCacheTransaction()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
endCacheTransaction()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      */
getCache(String url)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      */
removeCache(String url)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      */
addCache(String url, CacheResult c)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      */
clearCache()702     void clearCache() {
703         if (mCacheDatabase == null) {
704             return;
705         }
706 
707         mCacheDatabase.delete("cache", null, null);
708     }
709 
hasCache()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 
getCacheTotalSize()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 
trimCache(long amount)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 
getAllCacheFileNames()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      */
setUsernamePassword(String schemePlusHost, String username, String password)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      */
getUsernamePassword(String schemePlusHost)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      */
hasUsernamePassword()895     public boolean hasUsernamePassword() {
896         synchronized (mPasswordLock) {
897             return hasEntries(TABLE_PASSWORD_ID);
898         }
899     }
900 
901     /**
902      * Clear password database
903      */
clearUsernamePassword()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      */
setHttpAuthUsernamePassword(String host, String realm, String username, String password)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      */
getHttpAuthUsernamePassword(String host, String realm)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      */
hasHttpAuthUsernamePassword()992     public boolean hasHttpAuthUsernamePassword() {
993         synchronized (mHttpAuthLock) {
994             return hasEntries(TABLE_HTTPAUTH_ID);
995         }
996     }
997 
998     /**
999      * Clear HTTP authentication password database
1000      */
clearHttpAuthUsernamePassword()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      */
setFormData(String url, HashMap<String, String> formdata)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      */
getFormData(String url, String name)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      */
hasFormData()1122     public boolean hasFormData() {
1123         synchronized (mFormLock) {
1124             return hasEntries(TABLE_FORMURL_ID);
1125         }
1126     }
1127 
1128     /**
1129      * Clear form database
1130      */
clearFormData()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