mobile/android/base/sqlite/SQLiteBridge.java

changeset 0
6474c204b198
     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 +}

mercurial