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