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