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