| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.webkit; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.Map.Entry; |
| |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.DatabaseUtils; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteException; |
| import android.database.sqlite.SQLiteStatement; |
| import android.util.Log; |
| |
| public class WebViewDatabase { |
| private static final String DATABASE_FILE = "webview.db"; |
| private static final String CACHE_DATABASE_FILE = "webviewCache.db"; |
| |
| // log tag |
| protected static final String LOGTAG = "webviewdatabase"; |
| |
| private static final int DATABASE_VERSION = 11; |
| // 2 -> 3 Modified Cache table to allow cache of redirects |
| // 3 -> 4 Added Oma-Downloads table |
| // 4 -> 5 Modified Cache table to support persistent contentLength |
| // 5 -> 4 Removed Oma-Downoads table |
| // 5 -> 6 Add INDEX for cache table |
| // 6 -> 7 Change cache localPath from int to String |
| // 7 -> 8 Move cache to its own db |
| // 8 -> 9 Store both scheme and host when storing passwords |
| // 9 -> 10 Update httpauth table UNIQUE |
| // 10 -> 11 Drop cookies and cache now managed by the chromium stack, |
| // and update the form data table to use the new format |
| // implemented for b/5265606. |
| |
| private static WebViewDatabase mInstance = null; |
| |
| private static SQLiteDatabase mDatabase = null; |
| |
| // synchronize locks |
| private final Object mPasswordLock = new Object(); |
| private final Object mFormLock = new Object(); |
| private final Object mHttpAuthLock = new Object(); |
| |
| private static final String mTableNames[] = { |
| "password", "formurl", "formdata", "httpauth" |
| }; |
| |
| // Table ids (they are index to mTableNames) |
| private static final int TABLE_PASSWORD_ID = 0; |
| private static final int TABLE_FORMURL_ID = 1; |
| private static final int TABLE_FORMDATA_ID = 2; |
| private static final int TABLE_HTTPAUTH_ID = 3; |
| |
| // column id strings for "_id" which can be used by any table |
| private static final String ID_COL = "_id"; |
| |
| private static final String[] ID_PROJECTION = new String[] { |
| "_id" |
| }; |
| |
| // column id strings for "password" table |
| private static final String PASSWORD_HOST_COL = "host"; |
| private static final String PASSWORD_USERNAME_COL = "username"; |
| private static final String PASSWORD_PASSWORD_COL = "password"; |
| |
| // column id strings for "formurl" table |
| private static final String FORMURL_URL_COL = "url"; |
| |
| // column id strings for "formdata" table |
| private static final String FORMDATA_URLID_COL = "urlid"; |
| private static final String FORMDATA_NAME_COL = "name"; |
| private static final String FORMDATA_VALUE_COL = "value"; |
| |
| // column id strings for "httpauth" table |
| private static final String HTTPAUTH_HOST_COL = "host"; |
| private static final String HTTPAUTH_REALM_COL = "realm"; |
| private static final String HTTPAUTH_USERNAME_COL = "username"; |
| private static final String HTTPAUTH_PASSWORD_COL = "password"; |
| |
| // Initially true until the background thread completes. |
| private boolean mInitialized = false; |
| |
| private WebViewDatabase(final Context context) { |
| new Thread() { |
| @Override |
| public void run() { |
| init(context); |
| } |
| }.start(); |
| |
| // Singleton only, use getInstance() |
| } |
| |
| public static synchronized WebViewDatabase getInstance(Context context) { |
| if (mInstance == null) { |
| mInstance = new WebViewDatabase(context); |
| } |
| return mInstance; |
| } |
| |
| private synchronized void init(Context context) { |
| if (mInitialized) { |
| return; |
| } |
| |
| initDatabase(context); |
| // Before using the Chromium HTTP stack, we stored the WebKit cache in |
| // our own DB. Clean up the DB file if it's still around. |
| context.deleteDatabase(CACHE_DATABASE_FILE); |
| |
| // Thread done, notify. |
| mInitialized = true; |
| notify(); |
| } |
| |
| private void initDatabase(Context context) { |
| try { |
| mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null); |
| } catch (SQLiteException e) { |
| // try again by deleting the old db and create a new one |
| if (context.deleteDatabase(DATABASE_FILE)) { |
| mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, |
| null); |
| } |
| } |
| |
| // mDatabase should not be null, |
| // the only case is RequestAPI test has problem to create db |
| if (mDatabase == null) { |
| mInitialized = true; |
| notify(); |
| return; |
| } |
| |
| if (mDatabase.getVersion() != DATABASE_VERSION) { |
| mDatabase.beginTransactionNonExclusive(); |
| try { |
| upgradeDatabase(); |
| mDatabase.setTransactionSuccessful(); |
| } finally { |
| mDatabase.endTransaction(); |
| } |
| } |
| } |
| |
| private static void upgradeDatabase() { |
| upgradeDatabaseToV10(); |
| upgradeDatabaseFromV10ToV11(); |
| // Add future database upgrade functions here, one version at a |
| // time. |
| mDatabase.setVersion(DATABASE_VERSION); |
| } |
| |
| private static void upgradeDatabaseFromV10ToV11() { |
| int oldVersion = mDatabase.getVersion(); |
| |
| if (oldVersion >= 11) { |
| // Nothing to do. |
| return; |
| } |
| |
| // Clear out old java stack cookies - this data is now stored in |
| // a separate database managed by the Chrome stack. |
| mDatabase.execSQL("DROP TABLE IF EXISTS cookies"); |
| |
| // Likewise for the old cache table. |
| mDatabase.execSQL("DROP TABLE IF EXISTS cache"); |
| |
| // Update form autocomplete URLs to match new ICS formatting. |
| Cursor c = mDatabase.query(mTableNames[TABLE_FORMURL_ID], null, null, |
| null, null, null, null); |
| while (c.moveToNext()) { |
| String urlId = Long.toString(c.getLong(c.getColumnIndex(ID_COL))); |
| String url = c.getString(c.getColumnIndex(FORMURL_URL_COL)); |
| ContentValues cv = new ContentValues(1); |
| cv.put(FORMURL_URL_COL, WebTextView.urlForAutoCompleteData(url)); |
| mDatabase.update(mTableNames[TABLE_FORMURL_ID], cv, ID_COL + "=?", |
| new String[] { urlId }); |
| } |
| c.close(); |
| } |
| |
| private static void upgradeDatabaseToV10() { |
| int oldVersion = mDatabase.getVersion(); |
| |
| if (oldVersion >= 10) { |
| // Nothing to do. |
| return; |
| } |
| |
| if (oldVersion != 0) { |
| Log.i(LOGTAG, "Upgrading database from version " |
| + oldVersion + " to " |
| + DATABASE_VERSION + ", which will destroy old data"); |
| } |
| |
| if (9 == oldVersion) { |
| mDatabase.execSQL("DROP TABLE IF EXISTS " |
| + mTableNames[TABLE_HTTPAUTH_ID]); |
| mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID] |
| + " (" + ID_COL + " INTEGER PRIMARY KEY, " |
| + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL |
| + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " |
| + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" |
| + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL |
| + ") ON CONFLICT REPLACE);"); |
| return; |
| } |
| |
| mDatabase.execSQL("DROP TABLE IF EXISTS cookies"); |
| mDatabase.execSQL("DROP TABLE IF EXISTS cache"); |
| mDatabase.execSQL("DROP TABLE IF EXISTS " |
| + mTableNames[TABLE_FORMURL_ID]); |
| mDatabase.execSQL("DROP TABLE IF EXISTS " |
| + mTableNames[TABLE_FORMDATA_ID]); |
| mDatabase.execSQL("DROP TABLE IF EXISTS " |
| + mTableNames[TABLE_HTTPAUTH_ID]); |
| mDatabase.execSQL("DROP TABLE IF EXISTS " |
| + mTableNames[TABLE_PASSWORD_ID]); |
| |
| // formurl |
| mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMURL_ID] |
| + " (" + ID_COL + " INTEGER PRIMARY KEY, " + FORMURL_URL_COL |
| + " TEXT" + ");"); |
| |
| // formdata |
| mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMDATA_ID] |
| + " (" + ID_COL + " INTEGER PRIMARY KEY, " |
| + FORMDATA_URLID_COL + " INTEGER, " + FORMDATA_NAME_COL |
| + " TEXT, " + FORMDATA_VALUE_COL + " TEXT," + " UNIQUE (" |
| + FORMDATA_URLID_COL + ", " + FORMDATA_NAME_COL + ", " |
| + FORMDATA_VALUE_COL + ") ON CONFLICT IGNORE);"); |
| |
| // httpauth |
| mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID] |
| + " (" + ID_COL + " INTEGER PRIMARY KEY, " |
| + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL |
| + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " |
| + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" |
| + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL |
| + ") ON CONFLICT REPLACE);"); |
| // passwords |
| mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID] |
| + " (" + ID_COL + " INTEGER PRIMARY KEY, " |
| + PASSWORD_HOST_COL + " TEXT, " + PASSWORD_USERNAME_COL |
| + " TEXT, " + PASSWORD_PASSWORD_COL + " TEXT," + " UNIQUE (" |
| + PASSWORD_HOST_COL + ", " + PASSWORD_USERNAME_COL |
| + ") ON CONFLICT REPLACE);"); |
| } |
| |
| // Wait for the background initialization thread to complete and check the |
| // database creation status. |
| private boolean checkInitialized() { |
| synchronized (this) { |
| while (!mInitialized) { |
| try { |
| wait(); |
| } catch (InterruptedException e) { |
| Log.e(LOGTAG, "Caught exception while checking " + |
| "initialization"); |
| Log.e(LOGTAG, Log.getStackTraceString(e)); |
| } |
| } |
| } |
| return mDatabase != null; |
| } |
| |
| private boolean hasEntries(int tableId) { |
| if (!checkInitialized()) { |
| return false; |
| } |
| |
| Cursor cursor = null; |
| boolean ret = false; |
| try { |
| cursor = mDatabase.query(mTableNames[tableId], ID_PROJECTION, |
| null, null, null, null, null); |
| ret = cursor.moveToFirst() == true; |
| } catch (IllegalStateException e) { |
| Log.e(LOGTAG, "hasEntries", e); |
| } finally { |
| if (cursor != null) cursor.close(); |
| } |
| return ret; |
| } |
| |
| // |
| // password functions |
| // |
| |
| /** |
| * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique. |
| * |
| * @param schemePlusHost The scheme and host for the password |
| * @param username The username for the password. If it is null, it means |
| * password can't be saved. |
| * @param password The password |
| */ |
| void setUsernamePassword(String schemePlusHost, String username, |
| String password) { |
| if (schemePlusHost == null || !checkInitialized()) { |
| return; |
| } |
| |
| synchronized (mPasswordLock) { |
| final ContentValues c = new ContentValues(); |
| c.put(PASSWORD_HOST_COL, schemePlusHost); |
| c.put(PASSWORD_USERNAME_COL, username); |
| c.put(PASSWORD_PASSWORD_COL, password); |
| mDatabase.insert(mTableNames[TABLE_PASSWORD_ID], PASSWORD_HOST_COL, |
| c); |
| } |
| } |
| |
| /** |
| * Retrieve the username and password for a given host |
| * |
| * @param schemePlusHost The scheme and host which passwords applies to |
| * @return String[] if found, String[0] is username, which can be null and |
| * String[1] is password. Return null if it can't find anything. |
| */ |
| String[] getUsernamePassword(String schemePlusHost) { |
| if (schemePlusHost == null || !checkInitialized()) { |
| return null; |
| } |
| |
| final String[] columns = new String[] { |
| PASSWORD_USERNAME_COL, PASSWORD_PASSWORD_COL |
| }; |
| final String selection = "(" + PASSWORD_HOST_COL + " == ?)"; |
| synchronized (mPasswordLock) { |
| String[] ret = null; |
| Cursor cursor = null; |
| try { |
| cursor = mDatabase.query(mTableNames[TABLE_PASSWORD_ID], |
| columns, selection, new String[] { schemePlusHost }, null, |
| null, null); |
| if (cursor.moveToFirst()) { |
| ret = new String[2]; |
| ret[0] = cursor.getString( |
| cursor.getColumnIndex(PASSWORD_USERNAME_COL)); |
| ret[1] = cursor.getString( |
| cursor.getColumnIndex(PASSWORD_PASSWORD_COL)); |
| } |
| } catch (IllegalStateException e) { |
| Log.e(LOGTAG, "getUsernamePassword", e); |
| } finally { |
| if (cursor != null) cursor.close(); |
| } |
| return ret; |
| } |
| } |
| |
| /** |
| * Find out if there are any passwords saved. |
| * |
| * @return TRUE if there is passwords saved |
| */ |
| public boolean hasUsernamePassword() { |
| synchronized (mPasswordLock) { |
| return hasEntries(TABLE_PASSWORD_ID); |
| } |
| } |
| |
| /** |
| * Clear password database |
| */ |
| public void clearUsernamePassword() { |
| if (!checkInitialized()) { |
| return; |
| } |
| |
| synchronized (mPasswordLock) { |
| mDatabase.delete(mTableNames[TABLE_PASSWORD_ID], null, null); |
| } |
| } |
| |
| // |
| // http authentication password functions |
| // |
| |
| /** |
| * Set HTTP authentication password. Tuple (HTTPAUTH_HOST_COL, |
| * HTTPAUTH_REALM_COL, HTTPAUTH_USERNAME_COL) is unique. |
| * |
| * @param host The host for the password |
| * @param realm The realm for the password |
| * @param username The username for the password. If it is null, it means |
| * password can't be saved. |
| * @param password The password |
| */ |
| void setHttpAuthUsernamePassword(String host, String realm, String username, |
| String password) { |
| if (host == null || realm == null || !checkInitialized()) { |
| return; |
| } |
| |
| synchronized (mHttpAuthLock) { |
| final ContentValues c = new ContentValues(); |
| c.put(HTTPAUTH_HOST_COL, host); |
| c.put(HTTPAUTH_REALM_COL, realm); |
| c.put(HTTPAUTH_USERNAME_COL, username); |
| c.put(HTTPAUTH_PASSWORD_COL, password); |
| mDatabase.insert(mTableNames[TABLE_HTTPAUTH_ID], HTTPAUTH_HOST_COL, |
| c); |
| } |
| } |
| |
| /** |
| * Retrieve the HTTP authentication username and password for a given |
| * host+realm pair |
| * |
| * @param host The host the password applies to |
| * @param realm The realm the password applies to |
| * @return String[] if found, String[0] is username, which can be null and |
| * String[1] is password. Return null if it can't find anything. |
| */ |
| String[] getHttpAuthUsernamePassword(String host, String realm) { |
| if (host == null || realm == null || !checkInitialized()){ |
| return null; |
| } |
| |
| final String[] columns = new String[] { |
| HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL |
| }; |
| final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND (" |
| + HTTPAUTH_REALM_COL + " == ?)"; |
| synchronized (mHttpAuthLock) { |
| String[] ret = null; |
| Cursor cursor = null; |
| try { |
| cursor = mDatabase.query(mTableNames[TABLE_HTTPAUTH_ID], |
| columns, selection, new String[] { host, realm }, null, |
| null, null); |
| if (cursor.moveToFirst()) { |
| ret = new String[2]; |
| ret[0] = cursor.getString( |
| cursor.getColumnIndex(HTTPAUTH_USERNAME_COL)); |
| ret[1] = cursor.getString( |
| cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL)); |
| } |
| } catch (IllegalStateException e) { |
| Log.e(LOGTAG, "getHttpAuthUsernamePassword", e); |
| } finally { |
| if (cursor != null) cursor.close(); |
| } |
| return ret; |
| } |
| } |
| |
| /** |
| * Find out if there are any HTTP authentication passwords saved. |
| * |
| * @return TRUE if there are passwords saved |
| */ |
| public boolean hasHttpAuthUsernamePassword() { |
| synchronized (mHttpAuthLock) { |
| return hasEntries(TABLE_HTTPAUTH_ID); |
| } |
| } |
| |
| /** |
| * Clear HTTP authentication password database |
| */ |
| public void clearHttpAuthUsernamePassword() { |
| if (!checkInitialized()) { |
| return; |
| } |
| |
| synchronized (mHttpAuthLock) { |
| mDatabase.delete(mTableNames[TABLE_HTTPAUTH_ID], null, null); |
| } |
| } |
| |
| // |
| // form data functions |
| // |
| |
| /** |
| * Set form data for a site. Tuple (FORMDATA_URLID_COL, FORMDATA_NAME_COL, |
| * FORMDATA_VALUE_COL) is unique |
| * |
| * @param url The url of the site |
| * @param formdata The form data in HashMap |
| */ |
| void setFormData(String url, HashMap<String, String> formdata) { |
| if (url == null || formdata == null || !checkInitialized()) { |
| return; |
| } |
| |
| final String selection = "(" + FORMURL_URL_COL + " == ?)"; |
| synchronized (mFormLock) { |
| long urlid = -1; |
| Cursor cursor = null; |
| try { |
| cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID], |
| ID_PROJECTION, selection, new String[] { url }, null, null, |
| null); |
| if (cursor.moveToFirst()) { |
| urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); |
| } else { |
| ContentValues c = new ContentValues(); |
| c.put(FORMURL_URL_COL, url); |
| urlid = mDatabase.insert( |
| mTableNames[TABLE_FORMURL_ID], null, c); |
| } |
| } catch (IllegalStateException e) { |
| Log.e(LOGTAG, "setFormData", e); |
| } finally { |
| if (cursor != null) cursor.close(); |
| } |
| if (urlid >= 0) { |
| Set<Entry<String, String>> set = formdata.entrySet(); |
| Iterator<Entry<String, String>> iter = set.iterator(); |
| ContentValues map = new ContentValues(); |
| map.put(FORMDATA_URLID_COL, urlid); |
| while (iter.hasNext()) { |
| Entry<String, String> entry = iter.next(); |
| map.put(FORMDATA_NAME_COL, entry.getKey()); |
| map.put(FORMDATA_VALUE_COL, entry.getValue()); |
| mDatabase.insert(mTableNames[TABLE_FORMDATA_ID], null, map); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Get all the values for a form entry with "name" in a given site |
| * |
| * @param url The url of the site |
| * @param name The name of the form entry |
| * @return A list of values. Return empty list if nothing is found. |
| */ |
| ArrayList<String> getFormData(String url, String name) { |
| ArrayList<String> values = new ArrayList<String>(); |
| if (url == null || name == null || !checkInitialized()) { |
| return values; |
| } |
| |
| final String urlSelection = "(" + FORMURL_URL_COL + " == ?)"; |
| final String dataSelection = "(" + FORMDATA_URLID_COL + " == ?) AND (" |
| + FORMDATA_NAME_COL + " == ?)"; |
| synchronized (mFormLock) { |
| Cursor cursor = null; |
| try { |
| cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID], |
| ID_PROJECTION, urlSelection, new String[] { url }, null, |
| null, null); |
| while (cursor.moveToNext()) { |
| long urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); |
| Cursor dataCursor = null; |
| try { |
| dataCursor = mDatabase.query( |
| mTableNames[TABLE_FORMDATA_ID], |
| new String[] { ID_COL, FORMDATA_VALUE_COL }, |
| dataSelection, |
| new String[] { Long.toString(urlid), name }, |
| null, null, null); |
| if (dataCursor.moveToFirst()) { |
| int valueCol = dataCursor.getColumnIndex( |
| FORMDATA_VALUE_COL); |
| do { |
| values.add(dataCursor.getString(valueCol)); |
| } while (dataCursor.moveToNext()); |
| } |
| } catch (IllegalStateException e) { |
| Log.e(LOGTAG, "getFormData dataCursor", e); |
| } finally { |
| if (dataCursor != null) dataCursor.close(); |
| } |
| } |
| } catch (IllegalStateException e) { |
| Log.e(LOGTAG, "getFormData cursor", e); |
| } finally { |
| if (cursor != null) cursor.close(); |
| } |
| return values; |
| } |
| } |
| |
| /** |
| * Find out if there is form data saved. |
| * |
| * @return TRUE if there is form data in the database |
| */ |
| public boolean hasFormData() { |
| synchronized (mFormLock) { |
| return hasEntries(TABLE_FORMURL_ID); |
| } |
| } |
| |
| /** |
| * Clear form database |
| */ |
| public void clearFormData() { |
| if (!checkInitialized()) { |
| return; |
| } |
| |
| synchronized (mFormLock) { |
| mDatabase.delete(mTableNames[TABLE_FORMURL_ID], null, null); |
| mDatabase.delete(mTableNames[TABLE_FORMDATA_ID], null, null); |
| } |
| } |
| } |