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

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

mercurial