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