WebViewDatabase.java revision 09eac425c29bdd89d5805af514353f0d55c3b8d7
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        int oldVersion = mDatabase.getVersion();
331        if (oldVersion != 0) {
332            Log.i(LOGTAG, "Upgrading database from version "
333                    + oldVersion + " to "
334                    + DATABASE_VERSION + ", which will destroy old data");
335        }
336        boolean justPasswords = 8 == oldVersion && 9 == DATABASE_VERSION;
337        boolean justAuth = 9 == oldVersion && 10 == DATABASE_VERSION;
338        if (justAuth) {
339            mDatabase.execSQL("DROP TABLE IF EXISTS "
340                    + mTableNames[TABLE_HTTPAUTH_ID]);
341            mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID]
342                    + " (" + ID_COL + " INTEGER PRIMARY KEY, "
343                    + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL
344                    + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, "
345                    + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE ("
346                    + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL
347                    + ") ON CONFLICT REPLACE);");
348            return;
349        }
350
351        if (!justPasswords) {
352            mDatabase.execSQL("DROP TABLE IF EXISTS "
353                    + mTableNames[TABLE_COOKIES_ID]);
354            mDatabase.execSQL("DROP TABLE IF EXISTS cache");
355            mDatabase.execSQL("DROP TABLE IF EXISTS "
356                    + mTableNames[TABLE_FORMURL_ID]);
357            mDatabase.execSQL("DROP TABLE IF EXISTS "
358                    + mTableNames[TABLE_FORMDATA_ID]);
359            mDatabase.execSQL("DROP TABLE IF EXISTS "
360                    + mTableNames[TABLE_HTTPAUTH_ID]);
361        }
362        mDatabase.execSQL("DROP TABLE IF EXISTS "
363                + mTableNames[TABLE_PASSWORD_ID]);
364
365        mDatabase.setVersion(DATABASE_VERSION);
366
367        if (!justPasswords) {
368            // cookies
369            mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_COOKIES_ID]
370                    + " (" + ID_COL + " INTEGER PRIMARY KEY, "
371                    + COOKIES_NAME_COL + " TEXT, " + COOKIES_VALUE_COL
372                    + " TEXT, " + COOKIES_DOMAIN_COL + " TEXT, "
373                    + COOKIES_PATH_COL + " TEXT, " + COOKIES_EXPIRES_COL
374                    + " INTEGER, " + COOKIES_SECURE_COL + " INTEGER" + ");");
375            mDatabase.execSQL("CREATE INDEX cookiesIndex ON "
376                    + mTableNames[TABLE_COOKIES_ID] + " (path)");
377
378            // formurl
379            mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMURL_ID]
380                    + " (" + ID_COL + " INTEGER PRIMARY KEY, " + FORMURL_URL_COL
381                    + " TEXT" + ");");
382
383            // formdata
384            mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMDATA_ID]
385                    + " (" + ID_COL + " INTEGER PRIMARY KEY, "
386                    + FORMDATA_URLID_COL + " INTEGER, " + FORMDATA_NAME_COL
387                    + " TEXT, " + FORMDATA_VALUE_COL + " TEXT," + " UNIQUE ("
388                    + FORMDATA_URLID_COL + ", " + FORMDATA_NAME_COL + ", "
389                    + FORMDATA_VALUE_COL + ") ON CONFLICT IGNORE);");
390
391            // httpauth
392            mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID]
393                    + " (" + ID_COL + " INTEGER PRIMARY KEY, "
394                    + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL
395                    + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, "
396                    + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE ("
397                    + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL
398                    + ") ON CONFLICT REPLACE);");
399        }
400        // passwords
401        mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID]
402                + " (" + ID_COL + " INTEGER PRIMARY KEY, "
403                + PASSWORD_HOST_COL + " TEXT, " + PASSWORD_USERNAME_COL
404                + " TEXT, " + PASSWORD_PASSWORD_COL + " TEXT," + " UNIQUE ("
405                + PASSWORD_HOST_COL + ", " + PASSWORD_USERNAME_COL
406                + ") ON CONFLICT REPLACE);");
407    }
408
409    private static void upgradeCacheDatabase() {
410        int oldVersion = mCacheDatabase.getVersion();
411        if (oldVersion != 0) {
412            Log.i(LOGTAG, "Upgrading cache database from version "
413                    + oldVersion + " to "
414                    + DATABASE_VERSION + ", which will destroy all old data");
415        }
416        mCacheDatabase.execSQL("DROP TABLE IF EXISTS cache");
417        mCacheDatabase.setVersion(CACHE_DATABASE_VERSION);
418    }
419
420    private static void bootstrapCacheDatabase() {
421        if (mCacheDatabase != null) {
422            mCacheDatabase.execSQL("CREATE TABLE cache"
423                    + " (" + ID_COL + " INTEGER PRIMARY KEY, " + CACHE_URL_COL
424                    + " TEXT, " + CACHE_FILE_PATH_COL + " TEXT, "
425                    + CACHE_LAST_MODIFY_COL + " TEXT, " + CACHE_ETAG_COL
426                    + " TEXT, " + CACHE_EXPIRES_COL + " INTEGER, "
427                    + CACHE_EXPIRES_STRING_COL + " TEXT, "
428                    + CACHE_MIMETYPE_COL + " TEXT, " + CACHE_ENCODING_COL
429                    + " TEXT," + CACHE_HTTP_STATUS_COL + " INTEGER, "
430                    + CACHE_LOCATION_COL + " TEXT, " + CACHE_CONTENTLENGTH_COL
431                    + " INTEGER, " + CACHE_CONTENTDISPOSITION_COL + " TEXT, "
432                    + CACHE_CROSSDOMAIN_COL + " TEXT,"
433                    + " UNIQUE (" + CACHE_URL_COL + ") ON CONFLICT REPLACE);");
434            mCacheDatabase.execSQL("CREATE INDEX cacheUrlIndex ON cache ("
435                    + CACHE_URL_COL + ")");
436        }
437    }
438
439    // Wait for the background initialization thread to complete and check the
440    // database creation status.
441    private boolean checkInitialized() {
442        synchronized (this) {
443            while (!mInitialized) {
444                try {
445                    wait();
446                } catch (InterruptedException e) {
447                    Log.e(LOGTAG, "Caught exception while checking " +
448                                  "initialization");
449                    Log.e(LOGTAG, Log.getStackTraceString(e));
450                }
451            }
452        }
453        return mDatabase != null;
454    }
455
456    private boolean hasEntries(int tableId) {
457        if (!checkInitialized()) {
458            return false;
459        }
460
461        Cursor cursor = null;
462        boolean ret = false;
463        try {
464            cursor = mDatabase.query(mTableNames[tableId], ID_PROJECTION,
465                    null, null, null, null, null);
466            ret = cursor.moveToFirst() == true;
467        } catch (IllegalStateException e) {
468            Log.e(LOGTAG, "hasEntries", e);
469        } finally {
470            if (cursor != null) cursor.close();
471        }
472        return ret;
473    }
474
475    //
476    // cookies functions
477    //
478
479    /**
480     * Get cookies in the format of CookieManager.Cookie inside an ArrayList for
481     * a given domain
482     *
483     * @return ArrayList<Cookie> If nothing is found, return an empty list.
484     */
485    ArrayList<Cookie> getCookiesForDomain(String domain) {
486        ArrayList<Cookie> list = new ArrayList<Cookie>();
487        if (domain == null || !checkInitialized()) {
488            return list;
489        }
490
491        synchronized (mCookieLock) {
492            final String[] columns = new String[] {
493                    ID_COL, COOKIES_DOMAIN_COL, COOKIES_PATH_COL,
494                    COOKIES_NAME_COL, COOKIES_VALUE_COL, COOKIES_EXPIRES_COL,
495                    COOKIES_SECURE_COL
496            };
497            final String selection = "(" + COOKIES_DOMAIN_COL
498                    + " GLOB '*' || ?)";
499            Cursor cursor = null;
500            try {
501                cursor = mDatabase.query(mTableNames[TABLE_COOKIES_ID],
502                        columns, selection, new String[] { domain }, null, null,
503                        null);
504                if (cursor.moveToFirst()) {
505                    int domainCol = cursor.getColumnIndex(COOKIES_DOMAIN_COL);
506                    int pathCol = cursor.getColumnIndex(COOKIES_PATH_COL);
507                    int nameCol = cursor.getColumnIndex(COOKIES_NAME_COL);
508                    int valueCol = cursor.getColumnIndex(COOKIES_VALUE_COL);
509                    int expiresCol = cursor.getColumnIndex(COOKIES_EXPIRES_COL);
510                    int secureCol = cursor.getColumnIndex(COOKIES_SECURE_COL);
511                    do {
512                        Cookie cookie = new Cookie();
513                        cookie.domain = cursor.getString(domainCol);
514                        cookie.path = cursor.getString(pathCol);
515                        cookie.name = cursor.getString(nameCol);
516                        cookie.value = cursor.getString(valueCol);
517                        if (cursor.isNull(expiresCol)) {
518                            cookie.expires = -1;
519                        } else {
520                            cookie.expires = cursor.getLong(expiresCol);
521                        }
522                        cookie.secure = cursor.getShort(secureCol) != 0;
523                        cookie.mode = Cookie.MODE_NORMAL;
524                        list.add(cookie);
525                    } while (cursor.moveToNext());
526                }
527            } catch (IllegalStateException e) {
528                Log.e(LOGTAG, "getCookiesForDomain", e);
529            } finally {
530                if (cursor != null) cursor.close();
531            }
532            return list;
533        }
534    }
535
536    /**
537     * Delete cookies which matches (domain, path, name).
538     *
539     * @param domain If it is null, nothing happens.
540     * @param path If it is null, all the cookies match (domain) will be
541     *            deleted.
542     * @param name If it is null, all the cookies match (domain, path) will be
543     *            deleted.
544     */
545    void deleteCookies(String domain, String path, String name) {
546        if (domain == null || !checkInitialized()) {
547            return;
548        }
549
550        synchronized (mCookieLock) {
551            final String where = "(" + COOKIES_DOMAIN_COL + " == ?) AND ("
552                    + COOKIES_PATH_COL + " == ?) AND (" + COOKIES_NAME_COL
553                    + " == ?)";
554            mDatabase.delete(mTableNames[TABLE_COOKIES_ID], where,
555                    new String[] { domain, path, name });
556        }
557    }
558
559    /**
560     * Add a cookie to the database
561     *
562     * @param cookie
563     */
564    void addCookie(Cookie cookie) {
565        if (cookie.domain == null || cookie.path == null || cookie.name == null
566                || !checkInitialized()) {
567            return;
568        }
569
570        synchronized (mCookieLock) {
571            ContentValues cookieVal = new ContentValues();
572            cookieVal.put(COOKIES_DOMAIN_COL, cookie.domain);
573            cookieVal.put(COOKIES_PATH_COL, cookie.path);
574            cookieVal.put(COOKIES_NAME_COL, cookie.name);
575            cookieVal.put(COOKIES_VALUE_COL, cookie.value);
576            if (cookie.expires != -1) {
577                cookieVal.put(COOKIES_EXPIRES_COL, cookie.expires);
578            }
579            cookieVal.put(COOKIES_SECURE_COL, cookie.secure);
580            mDatabase.insert(mTableNames[TABLE_COOKIES_ID], null, cookieVal);
581        }
582    }
583
584    /**
585     * Whether there is any cookies in the database
586     *
587     * @return TRUE if there is cookie.
588     */
589    boolean hasCookies() {
590        synchronized (mCookieLock) {
591            return hasEntries(TABLE_COOKIES_ID);
592        }
593    }
594
595    /**
596     * Clear cookie database
597     */
598    void clearCookies() {
599        if (!checkInitialized()) {
600            return;
601        }
602
603        synchronized (mCookieLock) {
604            mDatabase.delete(mTableNames[TABLE_COOKIES_ID], null, null);
605        }
606    }
607
608    /**
609     * Clear session cookies, which means cookie doesn't have EXPIRES.
610     */
611    void clearSessionCookies() {
612        if (!checkInitialized()) {
613            return;
614        }
615
616        final String sessionExpired = COOKIES_EXPIRES_COL + " ISNULL";
617        synchronized (mCookieLock) {
618            mDatabase.delete(mTableNames[TABLE_COOKIES_ID], sessionExpired,
619                    null);
620        }
621    }
622
623    /**
624     * Clear expired cookies
625     *
626     * @param now Time for now
627     */
628    void clearExpiredCookies(long now) {
629        if (!checkInitialized()) {
630            return;
631        }
632
633        final String expires = COOKIES_EXPIRES_COL + " <= ?";
634        synchronized (mCookieLock) {
635            mDatabase.delete(mTableNames[TABLE_COOKIES_ID], expires,
636                    new String[] { Long.toString(now) });
637        }
638    }
639
640    //
641    // cache functions
642    //
643
644    // only called from WebViewWorkerThread
645    boolean startCacheTransaction() {
646        if (++mCacheTransactionRefcount == 1) {
647            if (!Thread.currentThread().equals(
648                    WebViewWorker.getHandler().getLooper().getThread())) {
649                Log.w(LOGTAG, "startCacheTransaction should be called from "
650                        + "WebViewWorkerThread instead of from "
651                        + Thread.currentThread().getName());
652            }
653            mCacheDatabase.beginTransactionNonExclusive();
654            return true;
655        }
656        return false;
657    }
658
659    // only called from WebViewWorkerThread
660    boolean endCacheTransaction() {
661        if (--mCacheTransactionRefcount == 0) {
662            if (!Thread.currentThread().equals(
663                    WebViewWorker.getHandler().getLooper().getThread())) {
664                Log.w(LOGTAG, "endCacheTransaction should be called from "
665                        + "WebViewWorkerThread instead of from "
666                        + Thread.currentThread().getName());
667            }
668            try {
669                mCacheDatabase.setTransactionSuccessful();
670            } finally {
671                mCacheDatabase.endTransaction();
672            }
673            return true;
674        }
675        return false;
676    }
677
678    /**
679     * Get a cache item.
680     *
681     * @param url The url
682     * @return CacheResult The CacheManager.CacheResult
683     */
684    CacheResult getCache(String url) {
685        assert !JniUtil.useChromiumHttpStack();
686
687        if (url == null || !checkInitialized()) {
688            return null;
689        }
690
691        Cursor cursor = null;
692        final String query = "SELECT filepath, lastmodify, etag, expires, "
693                + "expiresstring, mimetype, encoding, httpstatus, location, contentlength, "
694                + "contentdisposition, crossdomain FROM cache WHERE url = ?";
695        try {
696            cursor = mCacheDatabase.rawQuery(query, new String[] { url });
697            if (cursor.moveToFirst()) {
698                CacheResult ret = new CacheResult();
699                ret.localPath = cursor.getString(0);
700                ret.lastModified = cursor.getString(1);
701                ret.etag = cursor.getString(2);
702                ret.expires = cursor.getLong(3);
703                ret.expiresString = cursor.getString(4);
704                ret.mimeType = cursor.getString(5);
705                ret.encoding = cursor.getString(6);
706                ret.httpStatusCode = cursor.getInt(7);
707                ret.location = cursor.getString(8);
708                ret.contentLength = cursor.getLong(9);
709                ret.contentdisposition = cursor.getString(10);
710                ret.crossDomain = cursor.getString(11);
711                return ret;
712            }
713        } catch (IllegalStateException e) {
714            Log.e(LOGTAG, "getCache", e);
715        } finally {
716            if (cursor != null) cursor.close();
717        }
718        return null;
719    }
720
721    /**
722     * Remove a cache item.
723     *
724     * @param url The url
725     */
726    void removeCache(String url) {
727        assert !JniUtil.useChromiumHttpStack();
728
729        if (url == null || !checkInitialized()) {
730            return;
731        }
732
733        mCacheDatabase.execSQL("DELETE FROM cache WHERE url = ?", new String[] { url });
734    }
735
736    /**
737     * Add or update a cache. CACHE_URL_COL is unique in the table.
738     *
739     * @param url The url
740     * @param c The CacheManager.CacheResult
741     */
742    void addCache(String url, CacheResult c) {
743        assert !JniUtil.useChromiumHttpStack();
744
745        if (url == null || !checkInitialized()) {
746            return;
747        }
748
749        mCacheInserter.prepareForInsert();
750        mCacheInserter.bind(mCacheUrlColIndex, url);
751        mCacheInserter.bind(mCacheFilePathColIndex, c.localPath);
752        mCacheInserter.bind(mCacheLastModifyColIndex, c.lastModified);
753        mCacheInserter.bind(mCacheETagColIndex, c.etag);
754        mCacheInserter.bind(mCacheExpiresColIndex, c.expires);
755        mCacheInserter.bind(mCacheExpiresStringColIndex, c.expiresString);
756        mCacheInserter.bind(mCacheMimeTypeColIndex, c.mimeType);
757        mCacheInserter.bind(mCacheEncodingColIndex, c.encoding);
758        mCacheInserter.bind(mCacheHttpStatusColIndex, c.httpStatusCode);
759        mCacheInserter.bind(mCacheLocationColIndex, c.location);
760        mCacheInserter.bind(mCacheContentLengthColIndex, c.contentLength);
761        mCacheInserter.bind(mCacheContentDispositionColIndex,
762                c.contentdisposition);
763        mCacheInserter.bind(mCacheCrossDomainColIndex, c.crossDomain);
764        mCacheInserter.execute();
765    }
766
767    /**
768     * Clear cache database
769     */
770    void clearCache() {
771        if (!checkInitialized()) {
772            return;
773        }
774
775        mCacheDatabase.delete("cache", null, null);
776    }
777
778    boolean hasCache() {
779        if (!checkInitialized()) {
780            return false;
781        }
782
783        Cursor cursor = null;
784        boolean ret = false;
785        try {
786            cursor = mCacheDatabase.query("cache", ID_PROJECTION,
787                    null, null, null, null, null);
788            ret = cursor.moveToFirst() == true;
789        } catch (IllegalStateException e) {
790            Log.e(LOGTAG, "hasCache", e);
791        } finally {
792            if (cursor != null) cursor.close();
793        }
794        return ret;
795    }
796
797    long getCacheTotalSize() {
798        if (mCacheDatabase == null) {
799            return 0;
800        }
801        long size = 0;
802        Cursor cursor = null;
803        final String query = "SELECT SUM(contentlength) as sum FROM cache";
804        try {
805            cursor = mCacheDatabase.rawQuery(query, null);
806            if (cursor.moveToFirst()) {
807                size = cursor.getLong(0);
808            }
809        } catch (IllegalStateException e) {
810            Log.e(LOGTAG, "getCacheTotalSize", e);
811        } finally {
812            if (cursor != null) cursor.close();
813        }
814        return size;
815    }
816
817    List<String> trimCache(long amount) {
818        ArrayList<String> pathList = new ArrayList<String>(100);
819        Cursor cursor = null;
820        final String query = "SELECT contentlength, filepath FROM cache ORDER BY expires ASC";
821        try {
822            cursor = mCacheDatabase.rawQuery(query, null);
823            if (cursor.moveToFirst()) {
824                int batchSize = 100;
825                StringBuilder pathStr = new StringBuilder(20 + 16 * batchSize);
826                pathStr.append("DELETE FROM cache WHERE filepath IN (?");
827                for (int i = 1; i < batchSize; i++) {
828                    pathStr.append(", ?");
829                }
830                pathStr.append(")");
831                SQLiteStatement statement = null;
832                try {
833                    statement = mCacheDatabase.compileStatement(
834                            pathStr.toString());
835                    // as bindString() uses 1-based index, initialize index to 1
836                    int index = 1;
837                    do {
838                        long length = cursor.getLong(0);
839                        if (length == 0) {
840                            continue;
841                        }
842                        amount -= length;
843                        String filePath = cursor.getString(1);
844                        statement.bindString(index, filePath);
845                        pathList.add(filePath);
846                        if (index++ == batchSize) {
847                            statement.execute();
848                            statement.clearBindings();
849                            index = 1;
850                        }
851                    } while (cursor.moveToNext() && amount > 0);
852                    if (index > 1) {
853                        // there may be old bindings from the previous statement
854                        // if index is less than batchSize, which is Ok.
855                        statement.execute();
856                    }
857                } catch (IllegalStateException e) {
858                    Log.e(LOGTAG, "trimCache SQLiteStatement", e);
859                } finally {
860                    if (statement != null) statement.close();
861                }
862            }
863        } catch (IllegalStateException e) {
864            Log.e(LOGTAG, "trimCache Cursor", e);
865        } finally {
866            if (cursor != null) cursor.close();
867        }
868        return pathList;
869    }
870
871    List<String> getAllCacheFileNames() {
872        ArrayList<String> pathList = null;
873        Cursor cursor = null;
874        try {
875            cursor = mCacheDatabase.rawQuery("SELECT filepath FROM cache",
876                    null);
877            if (cursor != null && cursor.moveToFirst()) {
878                pathList = new ArrayList<String>(cursor.getCount());
879                do {
880                    pathList.add(cursor.getString(0));
881                } while (cursor.moveToNext());
882            }
883        } catch (IllegalStateException e) {
884            Log.e(LOGTAG, "getAllCacheFileNames", e);
885        } finally {
886            if (cursor != null) cursor.close();
887        }
888        return pathList;
889    }
890
891    //
892    // password functions
893    //
894
895    /**
896     * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique.
897     *
898     * @param schemePlusHost The scheme and host for the password
899     * @param username The username for the password. If it is null, it means
900     *            password can't be saved.
901     * @param password The password
902     */
903    void setUsernamePassword(String schemePlusHost, String username,
904                String password) {
905        if (schemePlusHost == null || !checkInitialized()) {
906            return;
907        }
908
909        synchronized (mPasswordLock) {
910            final ContentValues c = new ContentValues();
911            c.put(PASSWORD_HOST_COL, schemePlusHost);
912            c.put(PASSWORD_USERNAME_COL, username);
913            c.put(PASSWORD_PASSWORD_COL, password);
914            mDatabase.insert(mTableNames[TABLE_PASSWORD_ID], PASSWORD_HOST_COL,
915                    c);
916        }
917    }
918
919    /**
920     * Retrieve the username and password for a given host
921     *
922     * @param schemePlusHost The scheme and host which passwords applies to
923     * @return String[] if found, String[0] is username, which can be null and
924     *         String[1] is password. Return null if it can't find anything.
925     */
926    String[] getUsernamePassword(String schemePlusHost) {
927        if (schemePlusHost == null || !checkInitialized()) {
928            return null;
929        }
930
931        final String[] columns = new String[] {
932                PASSWORD_USERNAME_COL, PASSWORD_PASSWORD_COL
933        };
934        final String selection = "(" + PASSWORD_HOST_COL + " == ?)";
935        synchronized (mPasswordLock) {
936            String[] ret = null;
937            Cursor cursor = null;
938            try {
939                cursor = mDatabase.query(mTableNames[TABLE_PASSWORD_ID],
940                        columns, selection, new String[] { schemePlusHost }, null,
941                        null, null);
942                if (cursor.moveToFirst()) {
943                    ret = new String[2];
944                    ret[0] = cursor.getString(
945                            cursor.getColumnIndex(PASSWORD_USERNAME_COL));
946                    ret[1] = cursor.getString(
947                            cursor.getColumnIndex(PASSWORD_PASSWORD_COL));
948                }
949            } catch (IllegalStateException e) {
950                Log.e(LOGTAG, "getUsernamePassword", e);
951            } finally {
952                if (cursor != null) cursor.close();
953            }
954            return ret;
955        }
956    }
957
958    /**
959     * Find out if there are any passwords saved.
960     *
961     * @return TRUE if there is passwords saved
962     */
963    public boolean hasUsernamePassword() {
964        synchronized (mPasswordLock) {
965            return hasEntries(TABLE_PASSWORD_ID);
966        }
967    }
968
969    /**
970     * Clear password database
971     */
972    public void clearUsernamePassword() {
973        if (!checkInitialized()) {
974            return;
975        }
976
977        synchronized (mPasswordLock) {
978            mDatabase.delete(mTableNames[TABLE_PASSWORD_ID], null, null);
979        }
980    }
981
982    //
983    // http authentication password functions
984    //
985
986    /**
987     * Set HTTP authentication password. Tuple (HTTPAUTH_HOST_COL,
988     * HTTPAUTH_REALM_COL, HTTPAUTH_USERNAME_COL) is unique.
989     *
990     * @param host The host for the password
991     * @param realm The realm for the password
992     * @param username The username for the password. If it is null, it means
993     *            password can't be saved.
994     * @param password The password
995     */
996    void setHttpAuthUsernamePassword(String host, String realm, String username,
997            String password) {
998        if (host == null || realm == null || !checkInitialized()) {
999            return;
1000        }
1001
1002        synchronized (mHttpAuthLock) {
1003            final ContentValues c = new ContentValues();
1004            c.put(HTTPAUTH_HOST_COL, host);
1005            c.put(HTTPAUTH_REALM_COL, realm);
1006            c.put(HTTPAUTH_USERNAME_COL, username);
1007            c.put(HTTPAUTH_PASSWORD_COL, password);
1008            mDatabase.insert(mTableNames[TABLE_HTTPAUTH_ID], HTTPAUTH_HOST_COL,
1009                    c);
1010        }
1011    }
1012
1013    /**
1014     * Retrieve the HTTP authentication username and password for a given
1015     * host+realm pair
1016     *
1017     * @param host The host the password applies to
1018     * @param realm The realm the password applies to
1019     * @return String[] if found, String[0] is username, which can be null and
1020     *         String[1] is password. Return null if it can't find anything.
1021     */
1022    String[] getHttpAuthUsernamePassword(String host, String realm) {
1023        if (host == null || realm == null || !checkInitialized()){
1024            return null;
1025        }
1026
1027        final String[] columns = new String[] {
1028                HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL
1029        };
1030        final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND ("
1031                + HTTPAUTH_REALM_COL + " == ?)";
1032        synchronized (mHttpAuthLock) {
1033            String[] ret = null;
1034            Cursor cursor = null;
1035            try {
1036                cursor = mDatabase.query(mTableNames[TABLE_HTTPAUTH_ID],
1037                        columns, selection, new String[] { host, realm }, null,
1038                        null, null);
1039                if (cursor.moveToFirst()) {
1040                    ret = new String[2];
1041                    ret[0] = cursor.getString(
1042                            cursor.getColumnIndex(HTTPAUTH_USERNAME_COL));
1043                    ret[1] = cursor.getString(
1044                            cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL));
1045                }
1046            } catch (IllegalStateException e) {
1047                Log.e(LOGTAG, "getHttpAuthUsernamePassword", e);
1048            } finally {
1049                if (cursor != null) cursor.close();
1050            }
1051            return ret;
1052        }
1053    }
1054
1055    /**
1056     *  Find out if there are any HTTP authentication passwords saved.
1057     *
1058     * @return TRUE if there are passwords saved
1059     */
1060    public boolean hasHttpAuthUsernamePassword() {
1061        synchronized (mHttpAuthLock) {
1062            return hasEntries(TABLE_HTTPAUTH_ID);
1063        }
1064    }
1065
1066    /**
1067     * Clear HTTP authentication password database
1068     */
1069    public void clearHttpAuthUsernamePassword() {
1070        if (!checkInitialized()) {
1071            return;
1072        }
1073
1074        synchronized (mHttpAuthLock) {
1075            mDatabase.delete(mTableNames[TABLE_HTTPAUTH_ID], null, null);
1076        }
1077    }
1078
1079    //
1080    // form data functions
1081    //
1082
1083    /**
1084     * Set form data for a site. Tuple (FORMDATA_URLID_COL, FORMDATA_NAME_COL,
1085     * FORMDATA_VALUE_COL) is unique
1086     *
1087     * @param url The url of the site
1088     * @param formdata The form data in HashMap
1089     */
1090    void setFormData(String url, HashMap<String, String> formdata) {
1091        if (url == null || formdata == null || !checkInitialized()) {
1092            return;
1093        }
1094
1095        final String selection = "(" + FORMURL_URL_COL + " == ?)";
1096        synchronized (mFormLock) {
1097            long urlid = -1;
1098            Cursor cursor = null;
1099            try {
1100                cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID],
1101                        ID_PROJECTION, selection, new String[] { url }, null, null,
1102                        null);
1103                if (cursor.moveToFirst()) {
1104                    urlid = cursor.getLong(cursor.getColumnIndex(ID_COL));
1105                } else {
1106                    ContentValues c = new ContentValues();
1107                    c.put(FORMURL_URL_COL, url);
1108                    urlid = mDatabase.insert(
1109                            mTableNames[TABLE_FORMURL_ID], null, c);
1110                }
1111            } catch (IllegalStateException e) {
1112                Log.e(LOGTAG, "setFormData", e);
1113            } finally {
1114                if (cursor != null) cursor.close();
1115            }
1116            if (urlid >= 0) {
1117                Set<Entry<String, String>> set = formdata.entrySet();
1118                Iterator<Entry<String, String>> iter = set.iterator();
1119                ContentValues map = new ContentValues();
1120                map.put(FORMDATA_URLID_COL, urlid);
1121                while (iter.hasNext()) {
1122                    Entry<String, String> entry = iter.next();
1123                    map.put(FORMDATA_NAME_COL, entry.getKey());
1124                    map.put(FORMDATA_VALUE_COL, entry.getValue());
1125                    mDatabase.insert(mTableNames[TABLE_FORMDATA_ID], null, map);
1126                }
1127            }
1128        }
1129    }
1130
1131    /**
1132     * Get all the values for a form entry with "name" in a given site
1133     *
1134     * @param url The url of the site
1135     * @param name The name of the form entry
1136     * @return A list of values. Return empty list if nothing is found.
1137     */
1138    ArrayList<String> getFormData(String url, String name) {
1139        ArrayList<String> values = new ArrayList<String>();
1140        if (url == null || name == null || !checkInitialized()) {
1141            return values;
1142        }
1143
1144        final String urlSelection = "(" + FORMURL_URL_COL + " == ?)";
1145        final String dataSelection = "(" + FORMDATA_URLID_COL + " == ?) AND ("
1146                + FORMDATA_NAME_COL + " == ?)";
1147        synchronized (mFormLock) {
1148            Cursor cursor = null;
1149            try {
1150                cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID],
1151                        ID_PROJECTION, urlSelection, new String[] { url }, null,
1152                        null, null);
1153                if (cursor.moveToFirst()) {
1154                    long urlid = cursor.getLong(cursor.getColumnIndex(ID_COL));
1155                    Cursor dataCursor = null;
1156                    try {
1157                        dataCursor = mDatabase.query(
1158                                mTableNames[TABLE_FORMDATA_ID],
1159                                new String[] { ID_COL, FORMDATA_VALUE_COL },
1160                                dataSelection,
1161                                new String[] { Long.toString(urlid), name },
1162                                null, null, null);
1163                        if (dataCursor.moveToFirst()) {
1164                            int valueCol = dataCursor.getColumnIndex(
1165                                    FORMDATA_VALUE_COL);
1166                            do {
1167                                values.add(dataCursor.getString(valueCol));
1168                            } while (dataCursor.moveToNext());
1169                        }
1170                    } catch (IllegalStateException e) {
1171                        Log.e(LOGTAG, "getFormData dataCursor", e);
1172                    } finally {
1173                        if (dataCursor != null) dataCursor.close();
1174                    }
1175                }
1176            } catch (IllegalStateException e) {
1177                Log.e(LOGTAG, "getFormData cursor", e);
1178            } finally {
1179                if (cursor != null) cursor.close();
1180            }
1181            return values;
1182        }
1183    }
1184
1185    /**
1186     * Find out if there is form data saved.
1187     *
1188     * @return TRUE if there is form data in the database
1189     */
1190    public boolean hasFormData() {
1191        synchronized (mFormLock) {
1192            return hasEntries(TABLE_FORMURL_ID);
1193        }
1194    }
1195
1196    /**
1197     * Clear form database
1198     */
1199    public void clearFormData() {
1200        if (!checkInitialized()) {
1201            return;
1202        }
1203
1204        synchronized (mFormLock) {
1205            mDatabase.delete(mTableNames[TABLE_FORMURL_ID], null, null);
1206            mDatabase.delete(mTableNames[TABLE_FORMDATA_ID], null, null);
1207        }
1208    }
1209}
1210