chromium/android_webview/java/src/org/chromium/android_webview/HttpAuthDatabase.java

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.util.Log;

import org.chromium.android_webview.common.Lifetime;

/**
 * This database is used to support WebView's setHttpAuthUsernamePassword and
 * getHttpAuthUsernamePassword methods, and WebViewDatabase's clearHttpAuthUsernamePassword and
 * hasHttpAuthUsernamePassword methods.
 *
 * While this class is intended to be used as a singleton, this property is not enforced in this
 * layer, primarily for ease of testing. To line up with the classic implementation and behavior,
 * there is no specific handling and reporting when SQL errors occur.
 *
 * Note on thread-safety: As per the classic implementation, most API functions have thread safety
 * provided by the underlying SQLiteDatabase instance. The exception is database opening: this
 * is handled in the dedicated background thread, which also provides a performance gain
 * if triggered early on (e.g. as a side effect of CookieSyncManager.createInstance() call),
 * sufficiently in advance of the first blocking usage of the API.
 */
@Lifetime.Profile
public class HttpAuthDatabase {
    private static final String LOGTAG = "HttpAuthDatabase";

    private static final int DATABASE_VERSION = 1;

    private SQLiteDatabase mDatabase;

    private static final String ID_COL = "_id";

    private static final String[] ID_PROJECTION = new String[] {ID_COL};

    // column id strings for "httpauth" table
    private static final String HTTPAUTH_TABLE_NAME = "httpauth";
    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 false until the background thread completes. */
    private boolean mInitialized;

    private final Object mInitializedLock = new Object();

    /**
     * Creates and returns an instance of HttpAuthDatabase for the named file, and kicks-off
     * background initialization of that database.
     *
     * @param context the Context to use for opening the database
     * @param databaseFile Name of the file to be initialized.
     */
    public static HttpAuthDatabase newInstance(final Context context, final String databaseFile) {
        final HttpAuthDatabase httpAuthDatabase = new HttpAuthDatabase();
        new Thread() {
            @Override
            public void run() {
                httpAuthDatabase.initOnBackgroundThread(context, databaseFile);
            }
        }.start();
        return httpAuthDatabase;
    }

    // Prevent instantiation. Callers should use newInstance().
    private HttpAuthDatabase() {}

    /**
     * Initializes the databases and notifies any callers waiting on waitForInit.
     *
     * @param context the Context to use for opening the database
     * @param databaseFile Name of the file to be initialized.
     */
    private void initOnBackgroundThread(Context context, String databaseFile) {
        synchronized (mInitializedLock) {
            if (mInitialized) {
                return;
            }

            initDatabase(context, databaseFile);

            // Thread done, notify.
            mInitialized = true;
            mInitializedLock.notifyAll();
        }
    }

    /**
     * Opens the database, and upgrades it if necessary.
     *
     * @param context the Context to use for opening the database
     * @param databaseFile Name of the file to be initialized.
     */
    private void initDatabase(Context context, String databaseFile) {
        try {
            mDatabase = context.openOrCreateDatabase(databaseFile, 0, null);
        } catch (SQLiteException e) {
            // try again by deleting the old db and create a new one
            if (context.deleteDatabase(databaseFile)) {
                try {
                    mDatabase = context.openOrCreateDatabase(databaseFile, 0, null);
                } catch (SQLiteException ex) {
                    Log.e(LOGTAG, "Caught exception while trying init again", ex);
                }
            }
        }

        if (mDatabase == null) {
            // Not much we can do to recover at this point
            Log.e(LOGTAG, "Unable to open or create " + databaseFile);
            return;
        }

        if (mDatabase.getVersion() != DATABASE_VERSION) {
            mDatabase.beginTransactionNonExclusive();
            try {
                createTable();
                mDatabase.setTransactionSuccessful();
            } finally {
                mDatabase.endTransaction();
            }
        }
    }

    private void createTable() {
        mDatabase.execSQL(
                "CREATE TABLE "
                        + HTTPAUTH_TABLE_NAME
                        + " ("
                        + 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);");

        mDatabase.setVersion(DATABASE_VERSION);
    }

    /**
     * Waits for the background initialization thread to complete and check the database creation
     * status.
     *
     * @return true if the database was initialized, false otherwise
     */
    private boolean waitForInit() {
        synchronized (mInitializedLock) {
            while (!mInitialized) {
                try {
                    mInitializedLock.wait();
                } catch (InterruptedException e) {
                    Log.e(LOGTAG, "Caught exception while checking initialization", e);
                }
            }
        }
        return mDatabase != null;
    }

    /**
     * Sets the 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.
     * @param password the password
     */
    public void setHttpAuthUsernamePassword(
            String host, String realm, String username, String password) {
        if (host == null || realm == null || !waitForInit()) {
            return;
        }

        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(HTTPAUTH_TABLE_NAME, HTTPAUTH_HOST_COL, c);
    }

    /**
     * Retrieves the HTTP authentication username and password for a given host and realm pair. If
     * there are multiple username/password combinations for a host/realm, only the first one will
     * be returned.
     *
     * @param host the host the password applies to
     * @param realm the realm the password applies to
     * @return a String[] if found where String[0] is username (which can be null) and
     *         String[1] is password.  Null is returned if it can't find anything.
     */
    public String[] getHttpAuthUsernamePassword(String host, String realm) {
        if (host == null || realm == null || !waitForInit()) {
            return null;
        }

        final String[] columns = new String[] {HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL};
        final String selection =
                "(" + HTTPAUTH_HOST_COL + " == ?) AND " + "(" + HTTPAUTH_REALM_COL + " == ?)";

        String[] ret = null;
        Cursor cursor = null;
        try {
            cursor =
                    mDatabase.query(
                            HTTPAUTH_TABLE_NAME,
                            columns,
                            selection,
                            new String[] {host, realm},
                            null,
                            null,
                            null);
            if (cursor.moveToFirst()) {
                ret =
                        new String[] {
                            cursor.getString(cursor.getColumnIndexOrThrow(HTTPAUTH_USERNAME_COL)),
                            cursor.getString(cursor.getColumnIndexOrThrow(HTTPAUTH_PASSWORD_COL)),
                        };
            }
        } catch (IllegalStateException e) {
            Log.e(LOGTAG, "getHttpAuthUsernamePassword", e);
        } finally {
            if (cursor != null) cursor.close();
        }
        return ret;
    }

    /**
     * Determines if there are any HTTP authentication passwords saved.
     *
     * @return true if there are passwords saved
     */
    public boolean hasHttpAuthUsernamePassword() {
        if (!waitForInit()) {
            return false;
        }

        Cursor cursor = null;
        boolean ret = false;
        try {
            cursor =
                    mDatabase.query(
                            HTTPAUTH_TABLE_NAME, ID_PROJECTION, null, null, null, null, null);
            ret = cursor.moveToFirst();
        } catch (IllegalStateException e) {
            Log.e(LOGTAG, "hasEntries", e);
        } finally {
            if (cursor != null) cursor.close();
        }
        return ret;
    }

    /** Clears the HTTP authentication password database. */
    public void clearHttpAuthUsernamePassword() {
        if (!waitForInit()) {
            return;
        }
        mDatabase.delete(HTTPAUTH_TABLE_NAME, null, null);
    }
}