/* 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 valueNames = new ArrayList(); ArrayList valueBinds = new ArrayList(); ArrayList keyNames = new ArrayList(); for (Entry 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 valueNames = new ArrayList(); StringBuilder sb = new StringBuilder("UPDATE "); sb.append(table); sb.append(" SET "); boolean isFirst = true; for (Entry 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(); } } }