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