mobile/android/base/sqlite/SQLiteBridge.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko.sqlite;
michael@0 6
michael@0 7 import android.content.ContentValues;
michael@0 8 import android.database.Cursor;
michael@0 9 import android.database.sqlite.SQLiteDatabase;
michael@0 10 import android.database.sqlite.SQLiteException;
michael@0 11 import android.text.TextUtils;
michael@0 12 import android.util.Log;
michael@0 13
michael@0 14 import org.mozilla.gecko.mozglue.RobocopTarget;
michael@0 15
michael@0 16 import java.util.ArrayList;
michael@0 17 import java.util.Arrays;
michael@0 18 import java.util.Map.Entry;
michael@0 19
michael@0 20 /*
michael@0 21 * This class allows using the mozsqlite3 library included with Firefox
michael@0 22 * to read SQLite databases, instead of the Android SQLiteDataBase API,
michael@0 23 * which might use whatever outdated DB is present on the Android system.
michael@0 24 */
michael@0 25 public class SQLiteBridge {
michael@0 26 private static final String LOGTAG = "SQLiteBridge";
michael@0 27
michael@0 28 // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
michael@0 29 private String mDb;
michael@0 30
michael@0 31 // Pointer to the database if it was opened with openDatabase. 0 implies closed.
michael@0 32 protected volatile long mDbPointer = 0L;
michael@0 33
michael@0 34 // Values remembered after a query.
michael@0 35 private long[] mQueryResults;
michael@0 36
michael@0 37 private boolean mTransactionSuccess = false;
michael@0 38 private boolean mInTransaction = false;
michael@0 39
michael@0 40 private static final int RESULT_INSERT_ROW_ID = 0;
michael@0 41 private static final int RESULT_ROWS_CHANGED = 1;
michael@0 42
michael@0 43 // Shamelessly cribbed from db/sqlite3/src/moz.build.
michael@0 44 private static final int DEFAULT_PAGE_SIZE_BYTES = 32768;
michael@0 45
michael@0 46 // The same size we use elsewhere.
michael@0 47 private static final int MAX_WAL_SIZE_BYTES = 524288;
michael@0 48
michael@0 49 // JNI code in $(topdir)/mozglue/android/..
michael@0 50 private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery,
michael@0 51 String[] aParams,
michael@0 52 long[] aUpdateResult)
michael@0 53 throws SQLiteBridgeException;
michael@0 54 private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery,
michael@0 55 String[] aParams,
michael@0 56 long[] aUpdateResult)
michael@0 57 throws SQLiteBridgeException;
michael@0 58 private static native long openDatabase(String aDb)
michael@0 59 throws SQLiteBridgeException;
michael@0 60 private static native void closeDatabase(long aDb);
michael@0 61
michael@0 62 // Takes the path to the database we want to access.
michael@0 63 @RobocopTarget
michael@0 64 public SQLiteBridge(String aDb) throws SQLiteBridgeException {
michael@0 65 mDb = aDb;
michael@0 66 }
michael@0 67
michael@0 68 // Executes a simple line of sql.
michael@0 69 public void execSQL(String sql)
michael@0 70 throws SQLiteBridgeException {
michael@0 71 internalQuery(sql, null);
michael@0 72 }
michael@0 73
michael@0 74 // Executes a simple line of sql. Allow you to bind arguments
michael@0 75 public void execSQL(String sql, String[] bindArgs)
michael@0 76 throws SQLiteBridgeException {
michael@0 77 internalQuery(sql, bindArgs);
michael@0 78 }
michael@0 79
michael@0 80 // Executes a DELETE statement on the database
michael@0 81 public int delete(String table, String whereClause, String[] whereArgs)
michael@0 82 throws SQLiteBridgeException {
michael@0 83 StringBuilder sb = new StringBuilder("DELETE from ");
michael@0 84 sb.append(table);
michael@0 85 if (whereClause != null) {
michael@0 86 sb.append(" WHERE " + whereClause);
michael@0 87 }
michael@0 88
michael@0 89 internalQuery(sb.toString(), whereArgs);
michael@0 90 return (int)mQueryResults[RESULT_ROWS_CHANGED];
michael@0 91 }
michael@0 92
michael@0 93 public Cursor query(String table,
michael@0 94 String[] columns,
michael@0 95 String selection,
michael@0 96 String[] selectionArgs,
michael@0 97 String groupBy,
michael@0 98 String having,
michael@0 99 String orderBy,
michael@0 100 String limit)
michael@0 101 throws SQLiteBridgeException {
michael@0 102 StringBuilder sb = new StringBuilder("SELECT ");
michael@0 103 if (columns != null)
michael@0 104 sb.append(TextUtils.join(", ", columns));
michael@0 105 else
michael@0 106 sb.append(" * ");
michael@0 107
michael@0 108 sb.append(" FROM ");
michael@0 109 sb.append(table);
michael@0 110
michael@0 111 if (selection != null) {
michael@0 112 sb.append(" WHERE " + selection);
michael@0 113 }
michael@0 114
michael@0 115 if (groupBy != null) {
michael@0 116 sb.append(" GROUP BY " + groupBy);
michael@0 117 }
michael@0 118
michael@0 119 if (having != null) {
michael@0 120 sb.append(" HAVING " + having);
michael@0 121 }
michael@0 122
michael@0 123 if (orderBy != null) {
michael@0 124 sb.append(" ORDER BY " + orderBy);
michael@0 125 }
michael@0 126
michael@0 127 if (limit != null) {
michael@0 128 sb.append(" " + limit);
michael@0 129 }
michael@0 130
michael@0 131 return rawQuery(sb.toString(), selectionArgs);
michael@0 132 }
michael@0 133
michael@0 134 @RobocopTarget
michael@0 135 public Cursor rawQuery(String sql, String[] selectionArgs)
michael@0 136 throws SQLiteBridgeException {
michael@0 137 return internalQuery(sql, selectionArgs);
michael@0 138 }
michael@0 139
michael@0 140 public long insert(String table, String nullColumnHack, ContentValues values)
michael@0 141 throws SQLiteBridgeException {
michael@0 142 if (values == null)
michael@0 143 return 0;
michael@0 144
michael@0 145 ArrayList<String> valueNames = new ArrayList<String>();
michael@0 146 ArrayList<String> valueBinds = new ArrayList<String>();
michael@0 147 ArrayList<String> keyNames = new ArrayList<String>();
michael@0 148
michael@0 149 for (Entry<String, Object> value : values.valueSet()) {
michael@0 150 keyNames.add(value.getKey());
michael@0 151
michael@0 152 Object val = value.getValue();
michael@0 153 if (val == null) {
michael@0 154 valueNames.add("NULL");
michael@0 155 } else {
michael@0 156 valueNames.add("?");
michael@0 157 valueBinds.add(val.toString());
michael@0 158 }
michael@0 159 }
michael@0 160
michael@0 161 StringBuilder sb = new StringBuilder("INSERT into ");
michael@0 162 sb.append(table);
michael@0 163
michael@0 164 sb.append(" (");
michael@0 165 sb.append(TextUtils.join(", ", keyNames));
michael@0 166 sb.append(")");
michael@0 167
michael@0 168 // XXX - Do we need to bind these values?
michael@0 169 sb.append(" VALUES (");
michael@0 170 sb.append(TextUtils.join(", ", valueNames));
michael@0 171 sb.append(") ");
michael@0 172
michael@0 173 String[] binds = new String[valueBinds.size()];
michael@0 174 valueBinds.toArray(binds);
michael@0 175 internalQuery(sb.toString(), binds);
michael@0 176 return mQueryResults[RESULT_INSERT_ROW_ID];
michael@0 177 }
michael@0 178
michael@0 179 public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
michael@0 180 throws SQLiteBridgeException {
michael@0 181 if (values == null)
michael@0 182 return 0;
michael@0 183
michael@0 184 ArrayList<String> valueNames = new ArrayList<String>();
michael@0 185
michael@0 186 StringBuilder sb = new StringBuilder("UPDATE ");
michael@0 187 sb.append(table);
michael@0 188 sb.append(" SET ");
michael@0 189
michael@0 190 boolean isFirst = true;
michael@0 191
michael@0 192 for (Entry<String, Object> value : values.valueSet()) {
michael@0 193 if (isFirst)
michael@0 194 isFirst = false;
michael@0 195 else
michael@0 196 sb.append(", ");
michael@0 197
michael@0 198 sb.append(value.getKey());
michael@0 199
michael@0 200 Object val = value.getValue();
michael@0 201 if (val == null) {
michael@0 202 sb.append(" = NULL");
michael@0 203 } else {
michael@0 204 sb.append(" = ?");
michael@0 205 valueNames.add(val.toString());
michael@0 206 }
michael@0 207 }
michael@0 208
michael@0 209 if (!TextUtils.isEmpty(whereClause)) {
michael@0 210 sb.append(" WHERE ");
michael@0 211 sb.append(whereClause);
michael@0 212 valueNames.addAll(Arrays.asList(whereArgs));
michael@0 213 }
michael@0 214
michael@0 215 String[] binds = new String[valueNames.size()];
michael@0 216 valueNames.toArray(binds);
michael@0 217
michael@0 218 internalQuery(sb.toString(), binds);
michael@0 219 return (int)mQueryResults[RESULT_ROWS_CHANGED];
michael@0 220 }
michael@0 221
michael@0 222 public int getVersion()
michael@0 223 throws SQLiteBridgeException {
michael@0 224 Cursor cursor = internalQuery("PRAGMA user_version", null);
michael@0 225 int ret = -1;
michael@0 226 if (cursor != null) {
michael@0 227 cursor.moveToFirst();
michael@0 228 String version = cursor.getString(0);
michael@0 229 ret = Integer.parseInt(version);
michael@0 230 }
michael@0 231 return ret;
michael@0 232 }
michael@0 233
michael@0 234 // Do an SQL query, substituting the parameters in the query with the passed
michael@0 235 // parameters. The parameters are substituted in order: named parameters
michael@0 236 // are not supported.
michael@0 237 private Cursor internalQuery(String aQuery, String[] aParams)
michael@0 238 throws SQLiteBridgeException {
michael@0 239
michael@0 240 mQueryResults = new long[2];
michael@0 241 if (isOpen()) {
michael@0 242 return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults);
michael@0 243 }
michael@0 244 return sqliteCall(mDb, aQuery, aParams, mQueryResults);
michael@0 245 }
michael@0 246
michael@0 247 /*
michael@0 248 * The second two parameters here are just provided for compatibility with SQLiteDatabase
michael@0 249 * Support for them is not currently implemented.
michael@0 250 */
michael@0 251 public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
michael@0 252 throws SQLiteException {
michael@0 253 if (factory != null) {
michael@0 254 throw new RuntimeException("factory not supported.");
michael@0 255 }
michael@0 256 if (flags != 0) {
michael@0 257 throw new RuntimeException("flags not supported.");
michael@0 258 }
michael@0 259
michael@0 260 SQLiteBridge bridge = null;
michael@0 261 try {
michael@0 262 bridge = new SQLiteBridge(path);
michael@0 263 bridge.mDbPointer = SQLiteBridge.openDatabase(path);
michael@0 264 } catch (SQLiteBridgeException ex) {
michael@0 265 // Catch and rethrow as a SQLiteException to match SQLiteDatabase.
michael@0 266 throw new SQLiteException(ex.getMessage());
michael@0 267 }
michael@0 268
michael@0 269 prepareWAL(bridge);
michael@0 270
michael@0 271 return bridge;
michael@0 272 }
michael@0 273
michael@0 274 public void close() {
michael@0 275 if (isOpen()) {
michael@0 276 closeDatabase(mDbPointer);
michael@0 277 }
michael@0 278 mDbPointer = 0L;
michael@0 279 }
michael@0 280
michael@0 281 public boolean isOpen() {
michael@0 282 return mDbPointer != 0;
michael@0 283 }
michael@0 284
michael@0 285 public void beginTransaction() throws SQLiteBridgeException {
michael@0 286 if (inTransaction()) {
michael@0 287 throw new SQLiteBridgeException("Nested transactions are not supported");
michael@0 288 }
michael@0 289 execSQL("BEGIN EXCLUSIVE");
michael@0 290 mTransactionSuccess = false;
michael@0 291 mInTransaction = true;
michael@0 292 }
michael@0 293
michael@0 294 public void beginTransactionNonExclusive() throws SQLiteBridgeException {
michael@0 295 if (inTransaction()) {
michael@0 296 throw new SQLiteBridgeException("Nested transactions are not supported");
michael@0 297 }
michael@0 298 execSQL("BEGIN IMMEDIATE");
michael@0 299 mTransactionSuccess = false;
michael@0 300 mInTransaction = true;
michael@0 301 }
michael@0 302
michael@0 303 public void endTransaction() {
michael@0 304 if (!inTransaction())
michael@0 305 return;
michael@0 306
michael@0 307 try {
michael@0 308 if (mTransactionSuccess) {
michael@0 309 execSQL("COMMIT TRANSACTION");
michael@0 310 } else {
michael@0 311 execSQL("ROLLBACK TRANSACTION");
michael@0 312 }
michael@0 313 } catch(SQLiteBridgeException ex) {
michael@0 314 Log.e(LOGTAG, "Error ending transaction", ex);
michael@0 315 }
michael@0 316 mInTransaction = false;
michael@0 317 mTransactionSuccess = false;
michael@0 318 }
michael@0 319
michael@0 320 public void setTransactionSuccessful() throws SQLiteBridgeException {
michael@0 321 if (!inTransaction()) {
michael@0 322 throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction");
michael@0 323 }
michael@0 324 mTransactionSuccess = true;
michael@0 325 }
michael@0 326
michael@0 327 public boolean inTransaction() {
michael@0 328 return mInTransaction;
michael@0 329 }
michael@0 330
michael@0 331 @Override
michael@0 332 public void finalize() {
michael@0 333 if (isOpen()) {
michael@0 334 Log.e(LOGTAG, "Bridge finalized without closing the database");
michael@0 335 close();
michael@0 336 }
michael@0 337 }
michael@0 338
michael@0 339 private static void prepareWAL(final SQLiteBridge bridge) {
michael@0 340 // Prepare for WAL mode. If we can, we switch to journal_mode=WAL, then
michael@0 341 // set the checkpoint size appropriately. If we can't, then we fall back
michael@0 342 // to truncating and synchronous writes.
michael@0 343 final Cursor cursor = bridge.internalQuery("PRAGMA journal_mode=WAL", null);
michael@0 344 try {
michael@0 345 if (cursor.moveToFirst()) {
michael@0 346 String journalMode = cursor.getString(0);
michael@0 347 Log.d(LOGTAG, "Journal mode: " + journalMode);
michael@0 348 if ("wal".equals(journalMode)) {
michael@0 349 // Success! Let's make sure we autocheckpoint at a reasonable interval.
michael@0 350 final int pageSizeBytes = bridge.getPageSizeBytes();
michael@0 351 final int checkpointPageCount = MAX_WAL_SIZE_BYTES / pageSizeBytes;
michael@0 352 bridge.internalQuery("PRAGMA wal_autocheckpoint=" + checkpointPageCount, null).close();
michael@0 353 } else {
michael@0 354 if (!"truncate".equals(journalMode)) {
michael@0 355 Log.w(LOGTAG, "Unable to activate WAL journal mode. Using truncate instead.");
michael@0 356 bridge.internalQuery("PRAGMA journal_mode=TRUNCATE", null).close();
michael@0 357 }
michael@0 358 Log.w(LOGTAG, "Not using WAL mode: using synchronous=FULL instead.");
michael@0 359 bridge.internalQuery("PRAGMA synchronous=FULL", null).close();
michael@0 360 }
michael@0 361 }
michael@0 362 } finally {
michael@0 363 cursor.close();
michael@0 364 }
michael@0 365 }
michael@0 366
michael@0 367 private int getPageSizeBytes() {
michael@0 368 if (!isOpen()) {
michael@0 369 throw new IllegalStateException("Database not open.");
michael@0 370 }
michael@0 371
michael@0 372 final Cursor cursor = internalQuery("PRAGMA page_size", null);
michael@0 373 try {
michael@0 374 if (!cursor.moveToFirst()) {
michael@0 375 Log.w(LOGTAG, "Unable to retrieve page size.");
michael@0 376 return DEFAULT_PAGE_SIZE_BYTES;
michael@0 377 }
michael@0 378
michael@0 379 return cursor.getInt(0);
michael@0 380 } finally {
michael@0 381 cursor.close();
michael@0 382 }
michael@0 383 }
michael@0 384 }

mercurial