1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sqlite/SQLiteBridge.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,384 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sqlite; 1.9 + 1.10 +import android.content.ContentValues; 1.11 +import android.database.Cursor; 1.12 +import android.database.sqlite.SQLiteDatabase; 1.13 +import android.database.sqlite.SQLiteException; 1.14 +import android.text.TextUtils; 1.15 +import android.util.Log; 1.16 + 1.17 +import org.mozilla.gecko.mozglue.RobocopTarget; 1.18 + 1.19 +import java.util.ArrayList; 1.20 +import java.util.Arrays; 1.21 +import java.util.Map.Entry; 1.22 + 1.23 +/* 1.24 + * This class allows using the mozsqlite3 library included with Firefox 1.25 + * to read SQLite databases, instead of the Android SQLiteDataBase API, 1.26 + * which might use whatever outdated DB is present on the Android system. 1.27 + */ 1.28 +public class SQLiteBridge { 1.29 + private static final String LOGTAG = "SQLiteBridge"; 1.30 + 1.31 + // Path to the database. If this database was not opened with openDatabase, we reopen it every query. 1.32 + private String mDb; 1.33 + 1.34 + // Pointer to the database if it was opened with openDatabase. 0 implies closed. 1.35 + protected volatile long mDbPointer = 0L; 1.36 + 1.37 + // Values remembered after a query. 1.38 + private long[] mQueryResults; 1.39 + 1.40 + private boolean mTransactionSuccess = false; 1.41 + private boolean mInTransaction = false; 1.42 + 1.43 + private static final int RESULT_INSERT_ROW_ID = 0; 1.44 + private static final int RESULT_ROWS_CHANGED = 1; 1.45 + 1.46 + // Shamelessly cribbed from db/sqlite3/src/moz.build. 1.47 + private static final int DEFAULT_PAGE_SIZE_BYTES = 32768; 1.48 + 1.49 + // The same size we use elsewhere. 1.50 + private static final int MAX_WAL_SIZE_BYTES = 524288; 1.51 + 1.52 + // JNI code in $(topdir)/mozglue/android/.. 1.53 + private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery, 1.54 + String[] aParams, 1.55 + long[] aUpdateResult) 1.56 + throws SQLiteBridgeException; 1.57 + private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery, 1.58 + String[] aParams, 1.59 + long[] aUpdateResult) 1.60 + throws SQLiteBridgeException; 1.61 + private static native long openDatabase(String aDb) 1.62 + throws SQLiteBridgeException; 1.63 + private static native void closeDatabase(long aDb); 1.64 + 1.65 + // Takes the path to the database we want to access. 1.66 + @RobocopTarget 1.67 + public SQLiteBridge(String aDb) throws SQLiteBridgeException { 1.68 + mDb = aDb; 1.69 + } 1.70 + 1.71 + // Executes a simple line of sql. 1.72 + public void execSQL(String sql) 1.73 + throws SQLiteBridgeException { 1.74 + internalQuery(sql, null); 1.75 + } 1.76 + 1.77 + // Executes a simple line of sql. Allow you to bind arguments 1.78 + public void execSQL(String sql, String[] bindArgs) 1.79 + throws SQLiteBridgeException { 1.80 + internalQuery(sql, bindArgs); 1.81 + } 1.82 + 1.83 + // Executes a DELETE statement on the database 1.84 + public int delete(String table, String whereClause, String[] whereArgs) 1.85 + throws SQLiteBridgeException { 1.86 + StringBuilder sb = new StringBuilder("DELETE from "); 1.87 + sb.append(table); 1.88 + if (whereClause != null) { 1.89 + sb.append(" WHERE " + whereClause); 1.90 + } 1.91 + 1.92 + internalQuery(sb.toString(), whereArgs); 1.93 + return (int)mQueryResults[RESULT_ROWS_CHANGED]; 1.94 + } 1.95 + 1.96 + public Cursor query(String table, 1.97 + String[] columns, 1.98 + String selection, 1.99 + String[] selectionArgs, 1.100 + String groupBy, 1.101 + String having, 1.102 + String orderBy, 1.103 + String limit) 1.104 + throws SQLiteBridgeException { 1.105 + StringBuilder sb = new StringBuilder("SELECT "); 1.106 + if (columns != null) 1.107 + sb.append(TextUtils.join(", ", columns)); 1.108 + else 1.109 + sb.append(" * "); 1.110 + 1.111 + sb.append(" FROM "); 1.112 + sb.append(table); 1.113 + 1.114 + if (selection != null) { 1.115 + sb.append(" WHERE " + selection); 1.116 + } 1.117 + 1.118 + if (groupBy != null) { 1.119 + sb.append(" GROUP BY " + groupBy); 1.120 + } 1.121 + 1.122 + if (having != null) { 1.123 + sb.append(" HAVING " + having); 1.124 + } 1.125 + 1.126 + if (orderBy != null) { 1.127 + sb.append(" ORDER BY " + orderBy); 1.128 + } 1.129 + 1.130 + if (limit != null) { 1.131 + sb.append(" " + limit); 1.132 + } 1.133 + 1.134 + return rawQuery(sb.toString(), selectionArgs); 1.135 + } 1.136 + 1.137 + @RobocopTarget 1.138 + public Cursor rawQuery(String sql, String[] selectionArgs) 1.139 + throws SQLiteBridgeException { 1.140 + return internalQuery(sql, selectionArgs); 1.141 + } 1.142 + 1.143 + public long insert(String table, String nullColumnHack, ContentValues values) 1.144 + throws SQLiteBridgeException { 1.145 + if (values == null) 1.146 + return 0; 1.147 + 1.148 + ArrayList<String> valueNames = new ArrayList<String>(); 1.149 + ArrayList<String> valueBinds = new ArrayList<String>(); 1.150 + ArrayList<String> keyNames = new ArrayList<String>(); 1.151 + 1.152 + for (Entry<String, Object> value : values.valueSet()) { 1.153 + keyNames.add(value.getKey()); 1.154 + 1.155 + Object val = value.getValue(); 1.156 + if (val == null) { 1.157 + valueNames.add("NULL"); 1.158 + } else { 1.159 + valueNames.add("?"); 1.160 + valueBinds.add(val.toString()); 1.161 + } 1.162 + } 1.163 + 1.164 + StringBuilder sb = new StringBuilder("INSERT into "); 1.165 + sb.append(table); 1.166 + 1.167 + sb.append(" ("); 1.168 + sb.append(TextUtils.join(", ", keyNames)); 1.169 + sb.append(")"); 1.170 + 1.171 + // XXX - Do we need to bind these values? 1.172 + sb.append(" VALUES ("); 1.173 + sb.append(TextUtils.join(", ", valueNames)); 1.174 + sb.append(") "); 1.175 + 1.176 + String[] binds = new String[valueBinds.size()]; 1.177 + valueBinds.toArray(binds); 1.178 + internalQuery(sb.toString(), binds); 1.179 + return mQueryResults[RESULT_INSERT_ROW_ID]; 1.180 + } 1.181 + 1.182 + public int update(String table, ContentValues values, String whereClause, String[] whereArgs) 1.183 + throws SQLiteBridgeException { 1.184 + if (values == null) 1.185 + return 0; 1.186 + 1.187 + ArrayList<String> valueNames = new ArrayList<String>(); 1.188 + 1.189 + StringBuilder sb = new StringBuilder("UPDATE "); 1.190 + sb.append(table); 1.191 + sb.append(" SET "); 1.192 + 1.193 + boolean isFirst = true; 1.194 + 1.195 + for (Entry<String, Object> value : values.valueSet()) { 1.196 + if (isFirst) 1.197 + isFirst = false; 1.198 + else 1.199 + sb.append(", "); 1.200 + 1.201 + sb.append(value.getKey()); 1.202 + 1.203 + Object val = value.getValue(); 1.204 + if (val == null) { 1.205 + sb.append(" = NULL"); 1.206 + } else { 1.207 + sb.append(" = ?"); 1.208 + valueNames.add(val.toString()); 1.209 + } 1.210 + } 1.211 + 1.212 + if (!TextUtils.isEmpty(whereClause)) { 1.213 + sb.append(" WHERE "); 1.214 + sb.append(whereClause); 1.215 + valueNames.addAll(Arrays.asList(whereArgs)); 1.216 + } 1.217 + 1.218 + String[] binds = new String[valueNames.size()]; 1.219 + valueNames.toArray(binds); 1.220 + 1.221 + internalQuery(sb.toString(), binds); 1.222 + return (int)mQueryResults[RESULT_ROWS_CHANGED]; 1.223 + } 1.224 + 1.225 + public int getVersion() 1.226 + throws SQLiteBridgeException { 1.227 + Cursor cursor = internalQuery("PRAGMA user_version", null); 1.228 + int ret = -1; 1.229 + if (cursor != null) { 1.230 + cursor.moveToFirst(); 1.231 + String version = cursor.getString(0); 1.232 + ret = Integer.parseInt(version); 1.233 + } 1.234 + return ret; 1.235 + } 1.236 + 1.237 + // Do an SQL query, substituting the parameters in the query with the passed 1.238 + // parameters. The parameters are substituted in order: named parameters 1.239 + // are not supported. 1.240 + private Cursor internalQuery(String aQuery, String[] aParams) 1.241 + throws SQLiteBridgeException { 1.242 + 1.243 + mQueryResults = new long[2]; 1.244 + if (isOpen()) { 1.245 + return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults); 1.246 + } 1.247 + return sqliteCall(mDb, aQuery, aParams, mQueryResults); 1.248 + } 1.249 + 1.250 + /* 1.251 + * The second two parameters here are just provided for compatibility with SQLiteDatabase 1.252 + * Support for them is not currently implemented. 1.253 + */ 1.254 + public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags) 1.255 + throws SQLiteException { 1.256 + if (factory != null) { 1.257 + throw new RuntimeException("factory not supported."); 1.258 + } 1.259 + if (flags != 0) { 1.260 + throw new RuntimeException("flags not supported."); 1.261 + } 1.262 + 1.263 + SQLiteBridge bridge = null; 1.264 + try { 1.265 + bridge = new SQLiteBridge(path); 1.266 + bridge.mDbPointer = SQLiteBridge.openDatabase(path); 1.267 + } catch (SQLiteBridgeException ex) { 1.268 + // Catch and rethrow as a SQLiteException to match SQLiteDatabase. 1.269 + throw new SQLiteException(ex.getMessage()); 1.270 + } 1.271 + 1.272 + prepareWAL(bridge); 1.273 + 1.274 + return bridge; 1.275 + } 1.276 + 1.277 + public void close() { 1.278 + if (isOpen()) { 1.279 + closeDatabase(mDbPointer); 1.280 + } 1.281 + mDbPointer = 0L; 1.282 + } 1.283 + 1.284 + public boolean isOpen() { 1.285 + return mDbPointer != 0; 1.286 + } 1.287 + 1.288 + public void beginTransaction() throws SQLiteBridgeException { 1.289 + if (inTransaction()) { 1.290 + throw new SQLiteBridgeException("Nested transactions are not supported"); 1.291 + } 1.292 + execSQL("BEGIN EXCLUSIVE"); 1.293 + mTransactionSuccess = false; 1.294 + mInTransaction = true; 1.295 + } 1.296 + 1.297 + public void beginTransactionNonExclusive() throws SQLiteBridgeException { 1.298 + if (inTransaction()) { 1.299 + throw new SQLiteBridgeException("Nested transactions are not supported"); 1.300 + } 1.301 + execSQL("BEGIN IMMEDIATE"); 1.302 + mTransactionSuccess = false; 1.303 + mInTransaction = true; 1.304 + } 1.305 + 1.306 + public void endTransaction() { 1.307 + if (!inTransaction()) 1.308 + return; 1.309 + 1.310 + try { 1.311 + if (mTransactionSuccess) { 1.312 + execSQL("COMMIT TRANSACTION"); 1.313 + } else { 1.314 + execSQL("ROLLBACK TRANSACTION"); 1.315 + } 1.316 + } catch(SQLiteBridgeException ex) { 1.317 + Log.e(LOGTAG, "Error ending transaction", ex); 1.318 + } 1.319 + mInTransaction = false; 1.320 + mTransactionSuccess = false; 1.321 + } 1.322 + 1.323 + public void setTransactionSuccessful() throws SQLiteBridgeException { 1.324 + if (!inTransaction()) { 1.325 + throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction"); 1.326 + } 1.327 + mTransactionSuccess = true; 1.328 + } 1.329 + 1.330 + public boolean inTransaction() { 1.331 + return mInTransaction; 1.332 + } 1.333 + 1.334 + @Override 1.335 + public void finalize() { 1.336 + if (isOpen()) { 1.337 + Log.e(LOGTAG, "Bridge finalized without closing the database"); 1.338 + close(); 1.339 + } 1.340 + } 1.341 + 1.342 + private static void prepareWAL(final SQLiteBridge bridge) { 1.343 + // Prepare for WAL mode. If we can, we switch to journal_mode=WAL, then 1.344 + // set the checkpoint size appropriately. If we can't, then we fall back 1.345 + // to truncating and synchronous writes. 1.346 + final Cursor cursor = bridge.internalQuery("PRAGMA journal_mode=WAL", null); 1.347 + try { 1.348 + if (cursor.moveToFirst()) { 1.349 + String journalMode = cursor.getString(0); 1.350 + Log.d(LOGTAG, "Journal mode: " + journalMode); 1.351 + if ("wal".equals(journalMode)) { 1.352 + // Success! Let's make sure we autocheckpoint at a reasonable interval. 1.353 + final int pageSizeBytes = bridge.getPageSizeBytes(); 1.354 + final int checkpointPageCount = MAX_WAL_SIZE_BYTES / pageSizeBytes; 1.355 + bridge.internalQuery("PRAGMA wal_autocheckpoint=" + checkpointPageCount, null).close(); 1.356 + } else { 1.357 + if (!"truncate".equals(journalMode)) { 1.358 + Log.w(LOGTAG, "Unable to activate WAL journal mode. Using truncate instead."); 1.359 + bridge.internalQuery("PRAGMA journal_mode=TRUNCATE", null).close(); 1.360 + } 1.361 + Log.w(LOGTAG, "Not using WAL mode: using synchronous=FULL instead."); 1.362 + bridge.internalQuery("PRAGMA synchronous=FULL", null).close(); 1.363 + } 1.364 + } 1.365 + } finally { 1.366 + cursor.close(); 1.367 + } 1.368 + } 1.369 + 1.370 + private int getPageSizeBytes() { 1.371 + if (!isOpen()) { 1.372 + throw new IllegalStateException("Database not open."); 1.373 + } 1.374 + 1.375 + final Cursor cursor = internalQuery("PRAGMA page_size", null); 1.376 + try { 1.377 + if (!cursor.moveToFirst()) { 1.378 + Log.w(LOGTAG, "Unable to retrieve page size."); 1.379 + return DEFAULT_PAGE_SIZE_BYTES; 1.380 + } 1.381 + 1.382 + return cursor.getInt(0); 1.383 + } finally { 1.384 + cursor.close(); 1.385 + } 1.386 + } 1.387 +}