Skip to content

Commit 2adb82b

Browse files
Add support for Java API access to the several features:
SQLiteDatabase specific features: - WAL mode (enable, disable, check if enabled) - Get list of attached database - Perform an integrity check - Enable/disable foreign key constraints - A few different beginTransaction functions for immediate and exclusive modes SQLiteOpenHelper specific features: - Support onConfigure callback - Support onDowngrade callback - Expose getting database name - Expose setting WAL mode
1 parent 1c4265d commit 2adb82b

File tree

2 files changed

+243
-46
lines changed

2 files changed

+243
-46
lines changed

android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDatabase.java

Lines changed: 164 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.HashMap;
4040
import java.util.HashSet;
4141
import java.util.Iterator;
42+
import java.util.List;
4243
import java.util.Locale;
4344
import java.util.Map;
4445
import java.util.Set;
@@ -76,6 +77,12 @@ public class SQLiteDatabase extends SQLiteClosable {
7677
private static final int EVENT_DB_CORRUPT = 75004;
7778
private static final String KEY_ENCODING = "UTF-8";
7879

80+
private enum SQLiteDatabaseTransactionType {
81+
Deferred,
82+
Immediate,
83+
Exclusive,
84+
}
85+
7986
/**
8087
* The version number of the SQLCipher for Android Java client library.
8188
*/
@@ -635,6 +642,75 @@ private void checkLockHoldTime() {
635642
}
636643
}
637644

645+
/**
646+
* Performs a PRAGMA integrity_check; command against the database.
647+
* @return true if the integrity check is ok, otherwise false
648+
*/
649+
public boolean isDatabaseIntegrityOk() {
650+
Pair<Boolean, String> result = getResultFromPragma("PRAGMA integrity_check;");
651+
return result.first ? result.second.equals("ok") : result.first;
652+
}
653+
654+
/**
655+
* Returns a list of attached databases including the main database
656+
* by executing PRAGMA database_list
657+
* @return a list of pairs of database name and filename
658+
*/
659+
public List<Pair<String, String>> getAttachedDbs() {
660+
return getAttachedDbs(this);
661+
}
662+
663+
/**
664+
* Sets the journal mode of the database to WAL
665+
* @return true if successful, false otherwise
666+
*/
667+
public boolean enableWriteAheadLogging() {
668+
if(inTransaction()) {
669+
String message = "Write Ahead Logging cannot be enabled while in a transaction";
670+
throw new IllegalStateException(message);
671+
}
672+
List<Pair<String, String>> attachedDbs = getAttachedDbs(this);
673+
if(attachedDbs != null && attachedDbs.size() > 1) return false;
674+
if(isReadOnly() || getPath().equals(MEMORY)) return false;
675+
String command = "PRAGMA journal_mode = WAL;";
676+
rawExecSQL(command);
677+
return true;
678+
}
679+
680+
/**
681+
* Sets the journal mode of the database to DELETE (the default mode)
682+
*/
683+
public void disableWriteAheadLogging() {
684+
if(inTransaction()) {
685+
String message = "Write Ahead Logging cannot be disabled while in a transaction";
686+
throw new IllegalStateException(message);
687+
}
688+
String command = "PRAGMA journal_mode = DELETE;";
689+
rawExecSQL(command);
690+
}
691+
692+
/**
693+
* Sets the journal mode of the database to DELETE (the default mode)
694+
*/
695+
public boolean isWriteAheadLoggingEnabled() {
696+
Pair<Boolean, String> result = getResultFromPragma("PRAGMA journal_mode;");
697+
return result.first ? result.second.equals("wal") : result.first;
698+
}
699+
700+
/**
701+
* Enables or disables foreign key constraints
702+
* @param enable used to determine whether or not foreign key constraints are on
703+
*/
704+
public void setForeignKeyConstraintsEnabled(boolean enable) {
705+
if(inTransaction()) {
706+
String message = "Foreign key constraints may not be changed while in a transaction";
707+
throw new IllegalStateException(message);
708+
}
709+
String command = String.format("PRAGMA foreign_keys = %s;",
710+
enable ? "ON" : "OFF");
711+
execSQL(command);
712+
}
713+
638714
/**
639715
* Begins a transaction. Transactions can be nested. When the outer transaction is ended all of
640716
* the work done in that transaction and all of the nested transactions will be committed or
@@ -660,10 +736,12 @@ public void beginTransaction() {
660736
}
661737

662738
/**
663-
* Begins a transaction. Transactions can be nested. When the outer transaction is ended all of
664-
* the work done in that transaction and all of the nested transactions will be committed or
665-
* rolled back. The changes will be rolled back if any transaction is ended without being
666-
* marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
739+
* Begins a transaction in Exlcusive mode. Transactions can be nested. When
740+
* the outer transaction is ended all of the work done in that transaction
741+
* and all of the nested transactions will be committed or rolled back. The
742+
* changes will be rolled back if any transaction is ended without being
743+
* marked as clean (by calling setTransactionSuccessful). Otherwise they
744+
* will be committed.
667745
*
668746
* <p>Here is the standard idiom for transactions:
669747
*
@@ -683,47 +761,25 @@ public void beginTransaction() {
683761
* @throws IllegalStateException if the database is not open
684762
*/
685763
public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
686-
lockForced();
687-
if (!isOpen()) {
688-
throw new IllegalStateException("database not open");
689-
}
690-
boolean ok = false;
691-
try {
692-
// If this thread already had the lock then get out
693-
if (mLock.getHoldCount() > 1) {
694-
if (mInnerTransactionIsSuccessful) {
695-
String msg = "Cannot call beginTransaction between "
696-
+ "calling setTransactionSuccessful and endTransaction";
697-
IllegalStateException e = new IllegalStateException(msg);
698-
Log.e(TAG, "beginTransaction() failed", e);
699-
throw e;
700-
}
701-
ok = true;
702-
return;
703-
}
764+
beginTransactionWithListenerInternal(transactionListener,
765+
SQLiteDatabaseTransactionType.Exclusive);
766+
}
704767

705-
// This thread didn't already have the lock, so begin a database
706-
// transaction now.
707-
execSQL("BEGIN EXCLUSIVE;");
708-
mTransactionListener = transactionListener;
709-
mTransactionIsSuccessful = true;
710-
mInnerTransactionIsSuccessful = false;
711-
if (transactionListener != null) {
712-
try {
713-
transactionListener.onBegin();
714-
} catch (RuntimeException e) {
715-
execSQL("ROLLBACK;");
716-
throw e;
717-
}
718-
}
719-
ok = true;
720-
} finally {
721-
if (!ok) {
722-
// beginTransaction is called before the try block so we must release the lock in
723-
// the case of failure.
724-
unlockForced();
725-
}
726-
}
768+
/**
769+
* Begins a transaction in Immediate mode
770+
*/
771+
public void beginTransactionNonExclusive() {
772+
beginTransactionWithListenerInternal(null,
773+
SQLiteDatabaseTransactionType.Immediate);
774+
}
775+
776+
/**
777+
* Begins a transaction in Immediate mode
778+
* @param transactionListener is the listener used to report transaction events
779+
*/
780+
public void beginTransactionWithListenerNonExclusive(SQLiteTransactionListener transactionListener) {
781+
beginTransactionWithListenerInternal(transactionListener,
782+
SQLiteDatabaseTransactionType.Immediate);
727783
}
728784

729785
/**
@@ -2729,6 +2785,60 @@ public synchronized void setMaxSqlCacheSize(int cacheSize) {
27292785
mMaxSqlCacheSize = cacheSize;
27302786
}
27312787

2788+
private void beginTransactionWithListenerInternal(SQLiteTransactionListener transactionListener,
2789+
SQLiteDatabaseTransactionType transactionType) {
2790+
lockForced();
2791+
if (!isOpen()) {
2792+
throw new IllegalStateException("database not open");
2793+
}
2794+
boolean ok = false;
2795+
try {
2796+
// If this thread already had the lock then get out
2797+
if (mLock.getHoldCount() > 1) {
2798+
if (mInnerTransactionIsSuccessful) {
2799+
String msg = "Cannot call beginTransaction between "
2800+
+ "calling setTransactionSuccessful and endTransaction";
2801+
IllegalStateException e = new IllegalStateException(msg);
2802+
Log.e(TAG, "beginTransaction() failed", e);
2803+
throw e;
2804+
}
2805+
ok = true;
2806+
return;
2807+
}
2808+
// This thread didn't already have the lock, so begin a database
2809+
// transaction now.
2810+
if(transactionType == SQLiteDatabaseTransactionType.Exclusive) {
2811+
execSQL("BEGIN EXCLUSIVE;");
2812+
} else if(transactionType == SQLiteDatabaseTransactionType.Immediate) {
2813+
execSQL("BEGIN IMMEDIATE;");
2814+
} else if(transactionType == SQLiteDatabaseTransactionType.Deferred) {
2815+
execSQL("BEGIN DEFERRED;");
2816+
} else {
2817+
String message = String.format("%s is an unsupported transaction type",
2818+
transactionType);
2819+
throw new IllegalArgumentException(message);
2820+
}
2821+
mTransactionListener = transactionListener;
2822+
mTransactionIsSuccessful = true;
2823+
mInnerTransactionIsSuccessful = false;
2824+
if (transactionListener != null) {
2825+
try {
2826+
transactionListener.onBegin();
2827+
} catch (RuntimeException e) {
2828+
execSQL("ROLLBACK;");
2829+
throw e;
2830+
}
2831+
}
2832+
ok = true;
2833+
} finally {
2834+
if (!ok) {
2835+
// beginTransaction is called before the try block so we must release the lock in
2836+
// the case of failure.
2837+
unlockForced();
2838+
}
2839+
}
2840+
}
2841+
27322842
/**
27332843
* this method is used to collect data about ALL open databases in the current process.
27342844
* bugreport is a user of this data.
@@ -2837,6 +2947,16 @@ private byte[] getBytes(char[] data) {
28372947
return result;
28382948
}
28392949

2950+
private Pair<Boolean, String> getResultFromPragma(String command) {
2951+
Cursor cursor = rawQuery(command, new Object[]{});
2952+
if(cursor == null) return new Pair(false, "");
2953+
cursor.moveToFirst();
2954+
String value = cursor.getString(0);
2955+
cursor.close();
2956+
return new Pair(true, value);
2957+
}
2958+
2959+
28402960
/**
28412961
* Sets the root directory to search for the ICU data file
28422962
*/

android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteOpenHelper.java

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public abstract class SQLiteOpenHelper {
4343
private final int mNewVersion;
4444
private final SQLiteDatabaseHook mHook;
4545
private final DatabaseErrorHandler mErrorHandler;
46+
private boolean mEnableWriteAheadLogging;
4647

4748
private SQLiteDatabase mDatabase = null;
4849
private boolean mIsInitializing = false;
@@ -161,16 +162,19 @@ public synchronized SQLiteDatabase getWritableDatabase(char[] password) {
161162

162163
db = SQLiteDatabase.openOrCreateDatabase(path, password, mFactory, mHook, mErrorHandler);
163164
}
164-
165-
165+
onConfigure(db);
166166
int version = db.getVersion();
167167
if (version != mNewVersion) {
168168
db.beginTransaction();
169169
try {
170170
if (version == 0) {
171171
onCreate(db);
172172
} else {
173+
if(version > mNewVersion) {
174+
onDowngrade(db, version, mNewVersion);
175+
} else {
173176
onUpgrade(db, version, mNewVersion);
177+
}
174178
}
175179
db.setVersion(mNewVersion);
176180
db.setTransactionSuccessful();
@@ -274,6 +278,79 @@ public synchronized void close() {
274278
}
275279
}
276280

281+
/**
282+
* Return the name of the SQLite database being opened, as given to
283+
* the constructor.
284+
*/
285+
public String getDatabaseName() {
286+
return mName;
287+
}
288+
289+
/**
290+
* Enables or disables the use of write-ahead logging for the database.
291+
*
292+
* Write-ahead logging cannot be used with read-only databases so the value of
293+
* this flag is ignored if the database is opened read-only.
294+
*
295+
* @param enabled True if write-ahead logging should be enabled, false if it
296+
* should be disabled.
297+
*
298+
* @see SQLiteDatabase#enableWriteAheadLogging()
299+
*/
300+
public void setWriteAheadLoggingEnabled(boolean enabled) {
301+
synchronized (this) {
302+
if (mEnableWriteAheadLogging != enabled) {
303+
if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
304+
if (enabled) {
305+
mDatabase.enableWriteAheadLogging();
306+
} else {
307+
mDatabase.disableWriteAheadLogging();
308+
}
309+
}
310+
mEnableWriteAheadLogging = enabled;
311+
}
312+
}
313+
}
314+
315+
/**
316+
* Called when the database needs to be downgraded. This is strictly similar to
317+
* {@link #onUpgrade} method, but is called whenever current version is newer than requested one.
318+
* However, this method is not abstract, so it is not mandatory for a customer to
319+
* implement it. If not overridden, default implementation will reject downgrade and
320+
* throws SQLiteException
321+
*
322+
* <p>
323+
* This method executes within a transaction. If an exception is thrown, all changes
324+
* will automatically be rolled back.
325+
* </p>
326+
*
327+
* @param db The database.
328+
* @param oldVersion The old database version.
329+
* @param newVersion The new database version.
330+
*/
331+
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
332+
throw new SQLiteException("Can't downgrade database from version " +
333+
oldVersion + " to " + newVersion);
334+
}
335+
336+
/**
337+
* Called when the database connection is being configured, to enable features
338+
* such as write-ahead logging or foreign key support.
339+
* <p>
340+
* This method is called before {@link #onCreate}, {@link #onUpgrade},
341+
* {@link #onDowngrade}, or {@link #onOpen} are called. It should not modify
342+
* the database except to configure the database connection as required.
343+
* </p><p>
344+
* This method should only call methods that configure the parameters of the
345+
* database connection, such as {@link SQLiteDatabase#enableWriteAheadLogging}
346+
* {@link SQLiteDatabase#setForeignKeyConstraintsEnabled},
347+
* {@link SQLiteDatabase#setLocale}, or executing PRAGMA statements.
348+
* </p>
349+
*
350+
* @param db The database.
351+
*/
352+
public void onConfigure(SQLiteDatabase db) {}
353+
277354
/**
278355
* Called when the database is created for the first time. This is where the
279356
* creation of tables and the initial population of the tables should happen.

0 commit comments

Comments
 (0)