Port the SQLite locale setting code to Java.

Make the database opening code more robust in the case of
read-only database connections.

Check whether a PRAGMA needs to be issues before doing it.
Mostly it's harmless but it can grab a transaction on the
database unnecessarily.

Change-Id: Iab2cdc96c785e767f82966b00597e19337163f2f
diff --git a/core/java/android/database/SQLException.java b/core/java/android/database/SQLException.java
index 0386af0..3402026 100644
--- a/core/java/android/database/SQLException.java
+++ b/core/java/android/database/SQLException.java
@@ -19,12 +19,15 @@
 /**
  * An exception that indicates there was an error with SQL parsing or execution.
  */
-public class SQLException extends RuntimeException
-{
-    public SQLException() {}
+public class SQLException extends RuntimeException {
+    public SQLException() {
+    }
 
-    public SQLException(String error)
-    {
+    public SQLException(String error) {
         super(error);
     }
+
+    public SQLException(String error, Throwable cause) {
+        super(error, cause);
+    }
 }
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index 0db3e4f..e2c222b 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -99,6 +99,7 @@
     private final SQLiteDatabaseConfiguration mConfiguration;
     private final int mConnectionId;
     private final boolean mIsPrimaryConnection;
+    private final boolean mIsReadOnlyConnection;
     private final PreparedStatementCache mPreparedStatementCache;
     private PreparedStatement mPreparedStatementPool;
 
@@ -111,7 +112,7 @@
     private boolean mOnlyAllowReadOnlyOperations;
 
     // The number of times attachCancellationSignal has been called.
-    // Because SQLite statement execution can be re-entrant, we keep track of how many
+    // Because SQLite statement execution can be reentrant, we keep track of how many
     // times we have attempted to attach a cancellation signal to the connection so that
     // we can ensure that we detach the signal at the right time.
     private int mCancellationSignalAttachCount;
@@ -121,7 +122,7 @@
     private static native void nativeClose(int connectionPtr);
     private static native void nativeRegisterCustomFunction(int connectionPtr,
             SQLiteCustomFunction function);
-    private static native void nativeSetLocale(int connectionPtr, String locale);
+    private static native void nativeRegisterLocalizedCollators(int connectionPtr, String locale);
     private static native int nativePrepareStatement(int connectionPtr, String sql);
     private static native void nativeFinalizeStatement(int connectionPtr, int statementPtr);
     private static native int nativeGetParameterCount(int connectionPtr, int statementPtr);
@@ -163,6 +164,7 @@
         mConfiguration = new SQLiteDatabaseConfiguration(configuration);
         mConnectionId = connectionId;
         mIsPrimaryConnection = primaryConnection;
+        mIsReadOnlyConnection = (configuration.openFlags & SQLiteDatabase.OPEN_READONLY) != 0;
         mPreparedStatementCache = new PreparedStatementCache(
                 mConfiguration.maxSqlCacheSize);
         mCloseGuard.open("close");
@@ -237,45 +239,102 @@
     }
 
     private void setPageSize() {
-        if (!mConfiguration.isInMemoryDb()) {
-            execute("PRAGMA page_size=" + SQLiteGlobal.getDefaultPageSize(), null, null);
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final long newValue = SQLiteGlobal.getDefaultPageSize();
+            long value = executeForLong("PRAGMA page_size", null, null);
+            if (value != newValue) {
+                execute("PRAGMA page_size=" + newValue, null, null);
+            }
         }
     }
 
     private void setAutoCheckpointInterval() {
-        if (!mConfiguration.isInMemoryDb()) {
-            executeForLong("PRAGMA wal_autocheckpoint=" + SQLiteGlobal.getWALAutoCheckpoint(),
-                    null, null);
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final long newValue = SQLiteGlobal.getWALAutoCheckpoint();
+            long value = executeForLong("PRAGMA wal_autocheckpoint", null, null);
+            if (value != newValue) {
+                executeForLong("PRAGMA wal_autocheckpoint=" + newValue, null, null);
+            }
         }
     }
 
     private void setJournalSizeLimit() {
-        if (!mConfiguration.isInMemoryDb()) {
-            executeForLong("PRAGMA journal_size_limit=" + SQLiteGlobal.getJournalSizeLimit(),
-                    null, null);
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final long newValue = SQLiteGlobal.getJournalSizeLimit();
+            long value = executeForLong("PRAGMA journal_size_limit", null, null);
+            if (value != newValue) {
+                executeForLong("PRAGMA journal_size_limit=" + newValue, null, null);
+            }
         }
     }
 
     private void setSyncModeFromConfiguration() {
-        if (!mConfiguration.isInMemoryDb()) {
-            execute("PRAGMA synchronous=" + mConfiguration.syncMode, null, null);
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final String newValue = mConfiguration.syncMode;
+            String value = executeForString("PRAGMA synchronous", null, null);
+            if (!value.equalsIgnoreCase(newValue)) {
+                execute("PRAGMA synchronous=" + newValue, null, null);
+            }
         }
     }
 
     private void setJournalModeFromConfiguration() {
-        if (!mConfiguration.isInMemoryDb()) {
-            String result = executeForString("PRAGMA journal_mode=" + mConfiguration.journalMode,
-                    null, null);
-            if (!result.equalsIgnoreCase(mConfiguration.journalMode)) {
-                Log.e(TAG, "setting journal_mode to " + mConfiguration.journalMode
-                        + " failed for db: " + mConfiguration.label
-                        + " (on pragma set journal_mode, sqlite returned:" + result);
+        if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) {
+            final String newValue = mConfiguration.journalMode;
+            String value = executeForString("PRAGMA journal_mode", null, null);
+            if (!value.equalsIgnoreCase(newValue)) {
+                value = executeForString("PRAGMA journal_mode=" + newValue, null, null);
+                if (!value.equalsIgnoreCase(newValue)) {
+                    Log.e(TAG, "setting journal_mode to " + newValue
+                            + " failed for db: " + mConfiguration.label
+                            + " (on pragma set journal_mode, sqlite returned:" + value);
+                }
             }
         }
     }
 
     private void setLocaleFromConfiguration() {
-        nativeSetLocale(mConnectionPtr, mConfiguration.locale.toString());
+        if ((mConfiguration.openFlags & SQLiteDatabase.NO_LOCALIZED_COLLATORS) != 0) {
+            return;
+        }
+
+        // Register the localized collators.
+        final String newLocale = mConfiguration.locale.toString();
+        nativeRegisterLocalizedCollators(mConnectionPtr, newLocale);
+
+        // If the database is read-only, we cannot modify the android metadata table
+        // or existing indexes.
+        if (mIsReadOnlyConnection) {
+            return;
+        }
+
+        try {
+            // Ensure the android metadata table exists.
+            execute("CREATE TABLE IF NOT EXISTS android_metadata (locale TEXT)", null, null);
+
+            // Check whether the locale was actually changed.
+            final String oldLocale = executeForString("SELECT locale FROM android_metadata "
+                    + "UNION SELECT NULL ORDER BY locale DESC LIMIT 1", null, null);
+            if (oldLocale != null && oldLocale.equals(newLocale)) {
+                return;
+            }
+
+            // Go ahead and update the indexes using the new locale.
+            execute("BEGIN", null, null);
+            boolean success = false;
+            try {
+                execute("DELETE FROM android_metadata", null, null);
+                execute("INSERT INTO android_metadata (locale) VALUES(?)",
+                        new Object[] { newLocale }, null);
+                execute("REINDEX LOCALIZED", null, null);
+                success = true;
+            } finally {
+                execute(success ? "COMMIT" : "ROLLBACK", null, null);
+            }
+        } catch (RuntimeException ex) {
+            throw new SQLiteException("Failed to change locale for db '" + mConfiguration.label
+                    + "' to '" + newLocale + "'.", ex);
+        }
     }
 
     // Called by SQLiteConnectionPool only.
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index d41b484..bf32ea7 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -1718,7 +1718,7 @@
 
     /**
      * Sets the locale for this database.  Does nothing if this database has
-     * the NO_LOCALIZED_COLLATORS flag set or was opened read only.
+     * the {@link #NO_LOCALIZED_COLLATORS} flag set or was opened read only.
      *
      * @param locale The new locale.
      *
diff --git a/core/java/android/database/sqlite/SQLiteException.java b/core/java/android/database/sqlite/SQLiteException.java
index 3a97bfb..a1d9c9f 100644
--- a/core/java/android/database/sqlite/SQLiteException.java
+++ b/core/java/android/database/sqlite/SQLiteException.java
@@ -22,9 +22,14 @@
  * A SQLite exception that indicates there was an error with SQL parsing or execution.
  */
 public class SQLiteException extends SQLException {
-    public SQLiteException() {}
+    public SQLiteException() {
+    }
 
     public SQLiteException(String error) {
         super(error);
     }
+
+    public SQLiteException(String error, Throwable cause) {
+        super(error, cause);
+    }
 }
diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp
index c8f911f..fca5f20 100644
--- a/core/jni/android_database_SQLiteConnection.cpp
+++ b/core/jni/android_database_SQLiteConnection.cpp
@@ -36,8 +36,8 @@
 
 #include "android_database_SQLiteCommon.h"
 
+// Set to 1 to use UTF16 storage for localized indexes.
 #define UTF16_STORAGE 0
-#define ANDROID_TABLE "android_metadata"
 
 namespace android {
 
@@ -245,139 +245,16 @@
     }
 }
 
-// Set locale in the android_metadata table, install localized collators, and rebuild indexes
-static void nativeSetLocale(JNIEnv* env, jclass clazz, jint connectionPtr, jstring localeStr) {
+static void nativeRegisterLocalizedCollators(JNIEnv* env, jclass clazz, jint connectionPtr,
+        jstring localeStr) {
     SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
 
-    if (connection->openFlags & SQLiteConnection::NO_LOCALIZED_COLLATORS) {
-        // We should probably throw IllegalStateException but the contract for
-        // setLocale says that we just do nothing.  Oh well.
-        return;
-    }
+    const char* locale = env->GetStringUTFChars(localeStr, NULL);
+    int err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
+    env->ReleaseStringUTFChars(localeStr, locale);
 
-    int err;
-    char const* locale = env->GetStringUTFChars(localeStr, NULL);
-    sqlite3_stmt* stmt = NULL;
-    char** meta = NULL;
-    int rowCount, colCount;
-    char* dbLocale = NULL;
-
-    // create the table, if necessary and possible
-    if (!(connection->openFlags & SQLiteConnection::OPEN_READONLY)) {
-        err = sqlite3_exec(connection->db,
-                "CREATE TABLE IF NOT EXISTS " ANDROID_TABLE " (locale TEXT)",
-                NULL, NULL, NULL);
-        if (err != SQLITE_OK) {
-            ALOGE("CREATE TABLE " ANDROID_TABLE " failed");
-            throw_sqlite3_exception(env, connection->db);
-            goto done;
-        }
-    }
-
-    // try to read from the table
-    err = sqlite3_get_table(connection->db,
-            "SELECT locale FROM " ANDROID_TABLE " LIMIT 1",
-            &meta, &rowCount, &colCount, NULL);
     if (err != SQLITE_OK) {
-        ALOGE("SELECT locale FROM " ANDROID_TABLE " failed");
         throw_sqlite3_exception(env, connection->db);
-        goto done;
-    }
-
-    dbLocale = (rowCount >= 1) ? meta[colCount] : NULL;
-
-    if (dbLocale != NULL && !strcmp(dbLocale, locale)) {
-        // database locale is the same as the desired locale; set up the collators and go
-        err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
-        if (err != SQLITE_OK) {
-            throw_sqlite3_exception(env, connection->db);
-        }
-        goto done;   // no database changes needed
-    }
-
-    if (connection->openFlags & SQLiteConnection::OPEN_READONLY) {
-        // read-only database, so we're going to have to put up with whatever we got
-        // For registering new index. Not for modifing the read-only database.
-        err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
-        if (err != SQLITE_OK) {
-            throw_sqlite3_exception(env, connection->db);
-        }
-        goto done;
-    }
-
-    // need to update android_metadata and indexes atomically, so use a transaction...
-    err = sqlite3_exec(connection->db, "BEGIN TRANSACTION", NULL, NULL, NULL);
-    if (err != SQLITE_OK) {
-        ALOGE("BEGIN TRANSACTION failed setting locale");
-        throw_sqlite3_exception(env, connection->db);
-        goto done;
-    }
-
-    err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
-    if (err != SQLITE_OK) {
-        ALOGE("register_localized_collators() failed setting locale");
-        throw_sqlite3_exception(env, connection->db);
-        goto rollback;
-    }
-
-    err = sqlite3_exec(connection->db, "DELETE FROM " ANDROID_TABLE, NULL, NULL, NULL);
-    if (err != SQLITE_OK) {
-        ALOGE("DELETE failed setting locale");
-        throw_sqlite3_exception(env, connection->db);
-        goto rollback;
-    }
-
-    static const char *sql = "INSERT INTO " ANDROID_TABLE " (locale) VALUES(?);";
-    err = sqlite3_prepare_v2(connection->db, sql, -1, &stmt, NULL);
-    if (err != SQLITE_OK) {
-        ALOGE("sqlite3_prepare_v2(\"%s\") failed", sql);
-        throw_sqlite3_exception(env, connection->db);
-        goto rollback;
-    }
-
-    err = sqlite3_bind_text(stmt, 1, locale, -1, SQLITE_TRANSIENT);
-    if (err != SQLITE_OK) {
-        ALOGE("sqlite3_bind_text() failed setting locale");
-        throw_sqlite3_exception(env, connection->db);
-        goto rollback;
-    }
-
-    err = sqlite3_step(stmt);
-    if (err != SQLITE_OK && err != SQLITE_DONE) {
-        ALOGE("sqlite3_step(\"%s\") failed setting locale", sql);
-        throw_sqlite3_exception(env, connection->db);
-        goto rollback;
-    }
-
-    err = sqlite3_exec(connection->db, "REINDEX LOCALIZED", NULL, NULL, NULL);
-    if (err != SQLITE_OK) {
-        ALOGE("REINDEX LOCALIZED failed");
-        throw_sqlite3_exception(env, connection->db);
-        goto rollback;
-    }
-
-    // all done, yay!
-    err = sqlite3_exec(connection->db, "COMMIT TRANSACTION", NULL, NULL, NULL);
-    if (err != SQLITE_OK) {
-        ALOGE("COMMIT TRANSACTION failed setting locale");
-        throw_sqlite3_exception(env, connection->db);
-        goto done;
-    }
-
-rollback:
-    if (err != SQLITE_OK) {
-        sqlite3_exec(connection->db, "ROLLBACK TRANSACTION", NULL, NULL, NULL);
-    }
-
-done:
-    if (stmt) {
-        sqlite3_finalize(stmt);
-    }
-    if (meta) {
-        sqlite3_free_table(meta);
-    }
-    if (locale) {
-        env->ReleaseStringUTFChars(localeStr, locale);
     }
 }
 
@@ -898,8 +775,8 @@
             (void*)nativeClose },
     { "nativeRegisterCustomFunction", "(ILandroid/database/sqlite/SQLiteCustomFunction;)V",
             (void*)nativeRegisterCustomFunction },
-    { "nativeSetLocale", "(ILjava/lang/String;)V",
-            (void*)nativeSetLocale },
+    { "nativeRegisterLocalizedCollators", "(ILjava/lang/String;)V",
+            (void*)nativeRegisterLocalizedCollators },
     { "nativePrepareStatement", "(ILjava/lang/String;)I",
             (void*)nativePrepareStatement },
     { "nativeFinalizeStatement", "(II)V",