summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java387
1 files changed, 387 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
new file mode 100644
index 000000000..866b9e286
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
@@ -0,0 +1,387 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sqlite;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map.Entry;
+
+/*
+ * This class allows using the mozsqlite3 library included with Firefox
+ * to read SQLite databases, instead of the Android SQLiteDataBase API,
+ * which might use whatever outdated DB is present on the Android system.
+ */
+public class SQLiteBridge {
+ private static final String LOGTAG = "SQLiteBridge";
+
+ // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
+ private final String mDb;
+
+ // Pointer to the database if it was opened with openDatabase. 0 implies closed.
+ protected volatile long mDbPointer;
+
+ // Values remembered after a query.
+ private long[] mQueryResults;
+
+ private boolean mTransactionSuccess;
+ private boolean mInTransaction;
+
+ private static final int RESULT_INSERT_ROW_ID = 0;
+ private static final int RESULT_ROWS_CHANGED = 1;
+
+ // Shamelessly cribbed from db/sqlite3/src/moz.build.
+ private static final int DEFAULT_PAGE_SIZE_BYTES = 32768;
+
+ // The same size we use elsewhere.
+ private static final int MAX_WAL_SIZE_BYTES = 524288;
+
+ // JNI code in $(topdir)/mozglue/android/..
+ private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery,
+ String[] aParams,
+ long[] aUpdateResult)
+ throws SQLiteBridgeException;
+ private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery,
+ String[] aParams,
+ long[] aUpdateResult)
+ throws SQLiteBridgeException;
+ private static native long openDatabase(String aDb)
+ throws SQLiteBridgeException;
+ private static native void closeDatabase(long aDb);
+
+ // Takes the path to the database we want to access.
+ @RobocopTarget
+ public SQLiteBridge(String aDb) throws SQLiteBridgeException {
+ mDb = aDb;
+ }
+
+ // Executes a simple line of sql.
+ public void execSQL(String sql)
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery(sql, null);
+ cursor.close();
+ }
+
+ // Executes a simple line of sql. Allow you to bind arguments
+ public void execSQL(String sql, String[] bindArgs)
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery(sql, bindArgs);
+ cursor.close();
+ }
+
+ // Executes a DELETE statement on the database
+ public int delete(String table, String whereClause, String[] whereArgs)
+ throws SQLiteBridgeException {
+ StringBuilder sb = new StringBuilder("DELETE from ");
+ sb.append(table);
+ if (whereClause != null) {
+ sb.append(" WHERE " + whereClause);
+ }
+
+ execSQL(sb.toString(), whereArgs);
+ return (int)mQueryResults[RESULT_ROWS_CHANGED];
+ }
+
+ public Cursor query(String table,
+ String[] columns,
+ String selection,
+ String[] selectionArgs,
+ String groupBy,
+ String having,
+ String orderBy,
+ String limit)
+ throws SQLiteBridgeException {
+ StringBuilder sb = new StringBuilder("SELECT ");
+ if (columns != null)
+ sb.append(TextUtils.join(", ", columns));
+ else
+ sb.append(" * ");
+
+ sb.append(" FROM ");
+ sb.append(table);
+
+ if (selection != null) {
+ sb.append(" WHERE " + selection);
+ }
+
+ if (groupBy != null) {
+ sb.append(" GROUP BY " + groupBy);
+ }
+
+ if (having != null) {
+ sb.append(" HAVING " + having);
+ }
+
+ if (orderBy != null) {
+ sb.append(" ORDER BY " + orderBy);
+ }
+
+ if (limit != null) {
+ sb.append(" " + limit);
+ }
+
+ return rawQuery(sb.toString(), selectionArgs);
+ }
+
+ @RobocopTarget
+ public Cursor rawQuery(String sql, String[] selectionArgs)
+ throws SQLiteBridgeException {
+ return internalQuery(sql, selectionArgs);
+ }
+
+ public long insert(String table, String nullColumnHack, ContentValues values)
+ throws SQLiteBridgeException {
+ if (values == null)
+ return 0;
+
+ ArrayList<String> valueNames = new ArrayList<String>();
+ ArrayList<String> valueBinds = new ArrayList<String>();
+ ArrayList<String> keyNames = new ArrayList<String>();
+
+ for (Entry<String, Object> value : values.valueSet()) {
+ keyNames.add(value.getKey());
+
+ Object val = value.getValue();
+ if (val == null) {
+ valueNames.add("NULL");
+ } else {
+ valueNames.add("?");
+ valueBinds.add(val.toString());
+ }
+ }
+
+ StringBuilder sb = new StringBuilder("INSERT into ");
+ sb.append(table);
+
+ sb.append(" (");
+ sb.append(TextUtils.join(", ", keyNames));
+ sb.append(")");
+
+ // XXX - Do we need to bind these values?
+ sb.append(" VALUES (");
+ sb.append(TextUtils.join(", ", valueNames));
+ sb.append(") ");
+
+ String[] binds = new String[valueBinds.size()];
+ valueBinds.toArray(binds);
+ execSQL(sb.toString(), binds);
+ return mQueryResults[RESULT_INSERT_ROW_ID];
+ }
+
+ public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
+ throws SQLiteBridgeException {
+ if (values == null)
+ return 0;
+
+ ArrayList<String> valueNames = new ArrayList<String>();
+
+ StringBuilder sb = new StringBuilder("UPDATE ");
+ sb.append(table);
+ sb.append(" SET ");
+
+ boolean isFirst = true;
+
+ for (Entry<String, Object> value : values.valueSet()) {
+ if (isFirst)
+ isFirst = false;
+ else
+ sb.append(", ");
+
+ sb.append(value.getKey());
+
+ Object val = value.getValue();
+ if (val == null) {
+ sb.append(" = NULL");
+ } else {
+ sb.append(" = ?");
+ valueNames.add(val.toString());
+ }
+ }
+
+ if (!TextUtils.isEmpty(whereClause)) {
+ sb.append(" WHERE ");
+ sb.append(whereClause);
+ valueNames.addAll(Arrays.asList(whereArgs));
+ }
+
+ String[] binds = new String[valueNames.size()];
+ valueNames.toArray(binds);
+
+ execSQL(sb.toString(), binds);
+ return (int)mQueryResults[RESULT_ROWS_CHANGED];
+ }
+
+ public int getVersion()
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery("PRAGMA user_version", null);
+ int ret = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ String version = cursor.getString(0);
+ ret = Integer.parseInt(version);
+ cursor.close();
+ }
+ return ret;
+ }
+
+ // Do an SQL query, substituting the parameters in the query with the passed
+ // parameters. The parameters are substituted in order: named parameters
+ // are not supported.
+ private Cursor internalQuery(String aQuery, String[] aParams)
+ throws SQLiteBridgeException {
+
+ mQueryResults = new long[2];
+ if (isOpen()) {
+ return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults);
+ }
+ return sqliteCall(mDb, aQuery, aParams, mQueryResults);
+ }
+
+ /*
+ * The second two parameters here are just provided for compatibility with SQLiteDatabase
+ * Support for them is not currently implemented.
+ */
+ public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
+ throws SQLiteException {
+ if (factory != null) {
+ throw new RuntimeException("factory not supported.");
+ }
+ if (flags != 0) {
+ throw new RuntimeException("flags not supported.");
+ }
+
+ SQLiteBridge bridge = null;
+ try {
+ bridge = new SQLiteBridge(path);
+ bridge.mDbPointer = SQLiteBridge.openDatabase(path);
+ } catch (SQLiteBridgeException ex) {
+ // Catch and rethrow as a SQLiteException to match SQLiteDatabase.
+ throw new SQLiteException(ex.getMessage());
+ }
+
+ prepareWAL(bridge);
+
+ return bridge;
+ }
+
+ public void close() {
+ if (isOpen()) {
+ closeDatabase(mDbPointer);
+ }
+ mDbPointer = 0L;
+ }
+
+ public boolean isOpen() {
+ return mDbPointer != 0;
+ }
+
+ public void beginTransaction() throws SQLiteBridgeException {
+ if (inTransaction()) {
+ throw new SQLiteBridgeException("Nested transactions are not supported");
+ }
+ execSQL("BEGIN EXCLUSIVE");
+ mTransactionSuccess = false;
+ mInTransaction = true;
+ }
+
+ public void beginTransactionNonExclusive() throws SQLiteBridgeException {
+ if (inTransaction()) {
+ throw new SQLiteBridgeException("Nested transactions are not supported");
+ }
+ execSQL("BEGIN IMMEDIATE");
+ mTransactionSuccess = false;
+ mInTransaction = true;
+ }
+
+ public void endTransaction() {
+ if (!inTransaction())
+ return;
+
+ try {
+ if (mTransactionSuccess) {
+ execSQL("COMMIT TRANSACTION");
+ } else {
+ execSQL("ROLLBACK TRANSACTION");
+ }
+ } catch (SQLiteBridgeException ex) {
+ Log.e(LOGTAG, "Error ending transaction", ex);
+ }
+ mInTransaction = false;
+ mTransactionSuccess = false;
+ }
+
+ public void setTransactionSuccessful() throws SQLiteBridgeException {
+ if (!inTransaction()) {
+ throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction");
+ }
+ mTransactionSuccess = true;
+ }
+
+ public boolean inTransaction() {
+ return mInTransaction;
+ }
+
+ @Override
+ public void finalize() {
+ if (isOpen()) {
+ Log.e(LOGTAG, "Bridge finalized without closing the database");
+ close();
+ }
+ }
+
+ private static void prepareWAL(final SQLiteBridge bridge) {
+ // Prepare for WAL mode. If we can, we switch to journal_mode=WAL, then
+ // set the checkpoint size appropriately. If we can't, then we fall back
+ // to truncating and synchronous writes.
+ final Cursor cursor = bridge.internalQuery("PRAGMA journal_mode=WAL", null);
+ try {
+ if (cursor.moveToFirst()) {
+ String journalMode = cursor.getString(0);
+ Log.d(LOGTAG, "Journal mode: " + journalMode);
+ if ("wal".equals(journalMode)) {
+ // Success! Let's make sure we autocheckpoint at a reasonable interval.
+ final int pageSizeBytes = bridge.getPageSizeBytes();
+ final int checkpointPageCount = MAX_WAL_SIZE_BYTES / pageSizeBytes;
+ bridge.execSQL("PRAGMA wal_autocheckpoint=" + checkpointPageCount);
+ } else {
+ if (!"truncate".equals(journalMode)) {
+ Log.w(LOGTAG, "Unable to activate WAL journal mode. Using truncate instead.");
+ bridge.execSQL("PRAGMA journal_mode=TRUNCATE");
+ }
+ Log.w(LOGTAG, "Not using WAL mode: using synchronous=FULL instead.");
+ bridge.execSQL("PRAGMA synchronous=FULL");
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private int getPageSizeBytes() {
+ if (!isOpen()) {
+ throw new IllegalStateException("Database not open.");
+ }
+
+ final Cursor cursor = internalQuery("PRAGMA page_size", null);
+ try {
+ if (!cursor.moveToFirst()) {
+ Log.w(LOGTAG, "Unable to retrieve page size.");
+ return DEFAULT_PAGE_SIZE_BYTES;
+ }
+
+ return cursor.getInt(0);
+ } finally {
+ cursor.close();
+ }
+ }
+}