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