From 7f7e12b4f064d50e86092c149875767fbdf738f9 Mon Sep 17 00:00:00 2001 From: Stephen Lombardo Date: Mon, 24 Jun 2024 13:24:00 -0400 Subject: [PATCH 01/25] Adds support for 16 KB page sizes for android --- sqlcipher/src/main/jni/Application.mk | 1 + sqlcipher/src/main/jni/sqlcipher/Android.mk | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sqlcipher/src/main/jni/Application.mk b/sqlcipher/src/main/jni/Application.mk index d71dbdb..1ccba34 100644 --- a/sqlcipher/src/main/jni/Application.mk +++ b/sqlcipher/src/main/jni/Application.mk @@ -1 +1,2 @@ APP_STL:=c++_static +APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true diff --git a/sqlcipher/src/main/jni/sqlcipher/Android.mk b/sqlcipher/src/main/jni/sqlcipher/Android.mk index f245646..b9b46e1 100644 --- a/sqlcipher/src/main/jni/sqlcipher/Android.mk +++ b/sqlcipher/src/main/jni/sqlcipher/Android.mk @@ -32,6 +32,7 @@ LOCAL_C_INCLUDES += $(LOCAL_PATH) $(LOCAL_PATH)/nativehelper/ $(LOCAL_PATH)/andr LOCAL_MODULE:= libsqlcipher LOCAL_LDLIBS += -ldl -llog +LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384 LOCAL_STATIC_LIBRARIES += static-libcrypto include $(BUILD_SHARED_LIBRARY) @@ -39,4 +40,4 @@ include $(BUILD_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := static-libcrypto LOCAL_SRC_FILES := $(LOCAL_PATH)/android-libs/$(TARGET_ARCH_ABI)/libcrypto.a -include $(PREBUILT_STATIC_LIBRARY) \ No newline at end of file +include $(PREBUILT_STATIC_LIBRARY) From 1b75820828b9dec02ddbe8600cf0a18d269c5077 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Mon, 12 Aug 2024 11:31:51 -0500 Subject: [PATCH 02/25] Support binding null arguments for rawQuery --- .../database/sqlcipher_cts/SQLiteDatabaseTest.java | 12 ++++++++++++ .../zetetic/database/sqlcipher/SQLiteProgram.java | 5 +---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java index fe1f2c4..e49bd08 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java @@ -1490,4 +1490,16 @@ public void testEnableAndDisableForeignKeys() { mDatabase.setForeignKeyConstraintsEnabled(true); assertEquals(1, DatabaseUtils.longForQuery(mDatabase, "PRAGMA foreign_keys", null)); } + + public void testShouldSupportBindingNullValue(){ + String b = ""; + mDatabase.execSQL("CREATE TABLE t1(a,b);"); + mDatabase.execSQL("INSERT INTO t1 VALUES(?, ?);", new Object[]{null, "123"}); + Cursor cursor = mDatabase.rawQuery("SELECT * FROM t1 WHERE a is ?;", new Object[]{null}); + if(cursor != null && cursor.moveToNext()){ + b = cursor.getString(1); + cursor.close(); + } + assertEquals("123", b); + } } diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteProgram.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteProgram.java index c0836a2..330c473 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteProgram.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteProgram.java @@ -211,14 +211,11 @@ public void bindAllArgsAsStrings(String[] bindArgs) { /** * Given a varargs of Object bindArgs, this method binds all of them in one single call. * - * @param bindArgs the varargs of bind args, none of which must be null. + * @param bindArgs the varargs of bind args. */ public void bindAllArgs(Object... bindArgs){ if (bindArgs != null) { for (int i = bindArgs.length; i != 0; i--) { - if (bindArgs[i - 1] == null) { - throw new IllegalArgumentException("the bind value at index " + i + " is null"); - } bind(i, bindArgs[i - 1]); } } From 08e43d4622589f44243c9f788a938d9ba884d984 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 1 May 2023 16:23:58 -0400 Subject: [PATCH 03/25] Throw a specific exception for SQLITE_NOTADB. --- .../SQLiteNotADatabaseException.java | 22 +++++++++++++++++++ .../android_database_SQLiteCommon.cpp | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteNotADatabaseException.java diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteNotADatabaseException.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteNotADatabaseException.java new file mode 100644 index 0000000..82f0456 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteNotADatabaseException.java @@ -0,0 +1,22 @@ +package net.zetetic.database.sqlcipher; + +import android.database.sqlite.SQLiteException; + +/** + * An exception that is specific to the "SQLITE_NOTADB" error code. + * + * SQLITE_NOTADB + */ +class SQLiteNotADatabaseException extends SQLiteException { + public SQLiteNotADatabaseException() { + super(); + } + + public SQLiteNotADatabaseException(String error) { + super(error); + } + + public SQLiteNotADatabaseException(String error, Throwable cause) { + super(error, cause); + } +} diff --git a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteCommon.cpp b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteCommon.cpp index 3751f70..92471c9 100644 --- a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteCommon.cpp +++ b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteCommon.cpp @@ -74,7 +74,7 @@ void throw_sqlite3_exception(JNIEnv* env, int errcode, /* Upstream treats treat "unsupported file format" error as corruption (SQLiteDatabaseCorruptException). However, SQLITE_NOTADB can occur with mismatched keys, which is not a corruption case, so SQLCipher treats this as a general exception */ - exceptionClass = "android/database/sqlite/SQLiteException"; + exceptionClass = "net/zetetic/database/sqlcipher/SQLiteNotADatabaseException"; break; case SQLITE_CONSTRAINT: exceptionClass = "android/database/sqlite/SQLiteConstraintException"; From 7052792b229bcbaf41a8f1350964e8c55c7d2857 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 26 Aug 2024 08:59:43 -0400 Subject: [PATCH 04/25] Provide exception that indicated database corruption. --- .../sqlcipher_android/DatabaseGeneralTest.java | 10 +++++----- .../sqlcipher_cts/SQLCipherOpenHelperTest.java | 2 +- .../net/zetetic/database/DatabaseErrorHandler.java | 5 ++++- .../database/DefaultDatabaseErrorHandler.java | 2 +- .../zetetic/database/sqlcipher/SQLiteDatabase.java | 6 +++--- .../zetetic/database/sqlcipher/SQLiteProgram.java | 5 +++-- .../zetetic/database/sqlcipher/SQLiteQuery.java | 2 +- .../database/sqlcipher/SQLiteStatement.java | 14 +++++++------- 8 files changed, 25 insertions(+), 21 deletions(-) diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java index b043b35..38df84b 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java @@ -960,7 +960,7 @@ public void testDefaultDatabaseErrorHandler() { assertFalse(mDatabase.isOpen()); assertTrue(dbfile.exists()); try { - errorHandler.onCorruption(mDatabase); + errorHandler.onCorruption(mDatabase, new SQLiteException()); if(SQLiteDatabase.hasCodec()){ assertTrue(dbfile.exists()); } else { @@ -976,7 +976,7 @@ public void testDefaultDatabaseErrorHandler() { memoryDb.close(); assertFalse(memoryDb.isOpen()); try { - errorHandler.onCorruption(memoryDb); + errorHandler.onCorruption(memoryDb, new SQLiteException()); } catch (Exception e) { fail("unexpected"); } @@ -987,7 +987,7 @@ public void testDefaultDatabaseErrorHandler() { assertNotNull(dbObj); assertTrue(dbObj.isOpen()); try { - errorHandler.onCorruption(dbObj); + errorHandler.onCorruption(dbObj, new SQLiteException()); if(SQLiteDatabase.hasCodec()){ assertTrue(dbfile.exists()); } else{ @@ -1012,7 +1012,7 @@ public void testDefaultDatabaseErrorHandler() { assertTrue(dbObj.isOpen()); List> attachedDbs = dbObj.getAttachedDbs(); try { - errorHandler.onCorruption(dbObj); + errorHandler.onCorruption(dbObj, new SQLiteException()); if(SQLiteDatabase.hasCodec()){ assertTrue(dbfile.exists()); } else { @@ -1047,7 +1047,7 @@ public void testDefaultDatabaseErrorHandler() { assertTrue(dbObj.isOpen()); attachedDbs = dbObj.getAttachedDbs(); try { - errorHandler.onCorruption(dbObj); + errorHandler.onCorruption(dbObj, new SQLiteException()); if(SQLiteDatabase.hasCodec()){ assertTrue(dbfile.exists()); } else { diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherOpenHelperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherOpenHelperTest.java index e03377a..9f47658 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherOpenHelperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherOpenHelperTest.java @@ -72,7 +72,7 @@ private class SqlCipherOpenHelper extends SQLiteOpenHelper { private final String TAG = getClass().getSimpleName(); public SqlCipherOpenHelper(Context context) { - super(context, "test.db", "test", null, 1, 1, sqLiteDatabase -> Log.e(SQLCipherOpenHelperTest.this.TAG, "onCorruption()"), new SQLiteDatabaseHook() { + super(context, "test.db", "test", null, 1, 1, (sqLiteDatabase, ex) -> Log.e(SQLCipherOpenHelperTest.this.TAG, "onCorruption()"), new SQLiteDatabaseHook() { @Override public void preKey(SQLiteConnection sqLiteConnection) { Log.d(SQLCipherOpenHelperTest.this.TAG, "preKey()"); diff --git a/sqlcipher/src/main/java/net/zetetic/database/DatabaseErrorHandler.java b/sqlcipher/src/main/java/net/zetetic/database/DatabaseErrorHandler.java index 276fc65..2c049ea 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/DatabaseErrorHandler.java +++ b/sqlcipher/src/main/java/net/zetetic/database/DatabaseErrorHandler.java @@ -21,6 +21,8 @@ package net.zetetic.database; +import android.database.sqlite.SQLiteException; + import net.zetetic.database.sqlcipher.SQLiteDatabase; /** @@ -32,6 +34,7 @@ public interface DatabaseErrorHandler { * The method invoked when database corruption is detected. * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption * is detected. + * @param exception the exception reported by sqlite that indicated the database was corrupted. */ - void onCorruption(SQLiteDatabase dbObj); + void onCorruption(SQLiteDatabase dbObj, SQLiteException exception); } diff --git a/sqlcipher/src/main/java/net/zetetic/database/DefaultDatabaseErrorHandler.java b/sqlcipher/src/main/java/net/zetetic/database/DefaultDatabaseErrorHandler.java index b79bf73..d78b8ef 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/DefaultDatabaseErrorHandler.java +++ b/sqlcipher/src/main/java/net/zetetic/database/DefaultDatabaseErrorHandler.java @@ -56,7 +56,7 @@ public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption * is detected. */ - public void onCorruption(SQLiteDatabase dbObj) { + public void onCorruption(SQLiteDatabase dbObj, SQLiteException exception) { Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); // If this is a SEE build, do not delete any database files. diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java index cb13085..46dbb1b 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java @@ -343,9 +343,9 @@ String getLabel() { /** * Sends a corruption message to the database error handler. */ - void onCorruption() { + void onCorruption(SQLiteException exception) { EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel()); - mErrorHandler.onCorruption(this); + mErrorHandler.onCorruption(this, exception); } /** @@ -1012,7 +1012,7 @@ private void open() { try { openInner(); } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); openInner(); } } catch (SQLiteException ex) { diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteProgram.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteProgram.java index 330c473..75909e0 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteProgram.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteProgram.java @@ -21,6 +21,7 @@ package net.zetetic.database.sqlcipher; import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteException; import android.os.CancellationSignal; import androidx.sqlite.db.SupportSQLiteProgram; @@ -113,8 +114,8 @@ protected final int getConnectionFlags() { } /** @hide */ - protected final void onCorruption() { - mDatabase.onCorruption(); + protected final void onCorruption(SQLiteException exception) { + mDatabase.onCorruption(exception); } /** diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java index 028f38f..f81ac7b 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java @@ -70,7 +70,7 @@ window, startPos, requiredPos, countAllRows, getConnectionFlags(), mCancellationSignal); return numRows; } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); throw ex; } catch (SQLiteException ex) { Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql()); diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteStatement.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteStatement.java index 01b0f97..889266a 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteStatement.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteStatement.java @@ -50,7 +50,7 @@ public void executeRaw() { try { getSession().executeRaw(getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); throw ex; } finally { releaseReference(); @@ -69,7 +69,7 @@ public void execute() { try { getSession().execute(getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); throw ex; } finally { releaseReference(); @@ -90,7 +90,7 @@ public int executeUpdateDelete() { return getSession().executeForChangedRowCount( getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); throw ex; } finally { releaseReference(); @@ -112,7 +112,7 @@ public long executeInsert() { return getSession().executeForLastInsertedRowId( getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); throw ex; } finally { releaseReference(); @@ -133,7 +133,7 @@ public long simpleQueryForLong() { return getSession().executeForLong( getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); throw ex; } finally { releaseReference(); @@ -154,7 +154,7 @@ public String simpleQueryForString() { return getSession().executeForString( getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); throw ex; } finally { releaseReference(); @@ -175,7 +175,7 @@ public ParcelFileDescriptor simpleQueryForBlobFileDescriptor() { return getSession().executeForBlobFileDescriptor( getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); + onCorruption(ex); throw ex; } finally { releaseReference(); From 44338e95cda7a76d4494638a4c2215835118f1dd Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Wed, 28 Aug 2024 15:04:42 -0500 Subject: [PATCH 05/25] Bump cipher_version test to 4.6.1 --- .../zetetic/database/sqlcipher_cts/SQLCipherVersionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherVersionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherVersionTest.java index 2afa9f5..1ed9111 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherVersionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherVersionTest.java @@ -17,6 +17,6 @@ public void shouldExtractLibraryCipherVersion() { cipherVersion = cursor.getString(0); cursor.close(); } - assertThat(cipherVersion, containsString("4.6.0")); + assertThat(cipherVersion, containsString("4.6.1")); } } From ce1d19ffae1f48e188eeb4b07ae2aeb680ba33d1 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Wed, 4 Sep 2024 15:37:24 -0500 Subject: [PATCH 06/25] Prevent NullPointerException when calling delete with null parameter for where args --- .../sqlcipher_cts/SQLCipherDatabaseTest.java | 17 +++++++++++++++++ .../database/sqlcipher/SQLiteDatabase.java | 10 ++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java index 4f00506..eff3175 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java @@ -504,4 +504,21 @@ public void shouldAllowCursorWindowToResize(){ SQLiteCursor.resetCursorWindowSize(); } } + + @Test + public void shouldSupportDeleteWithNullWhereArgs(){ + long rowsFound = -1L; + database.execSQL("create table t1(a,b);"); + Object[] whereArgs = null; + database.execSQL("insert into t1(a,b) values(?,?)", new Object[]{1, 2}); + database.execSQL("insert into t1(a,b) values(?,?)", new Object[]{3, 4}); + long rowsDeleted = database.delete("t1", "a in (1, 3)", whereArgs); + assertThat(rowsDeleted, is(2L)); + Cursor cursor = database.rawQuery("select count(*) from t1;", null); + if(cursor != null && cursor.moveToNext()){ + rowsFound = cursor.getLong(0); + cursor.close(); + } + assertThat(rowsFound, is(0L)); + } } diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java index 46dbb1b..2b5755f 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java @@ -1180,8 +1180,9 @@ public long insert(String table, int conflictAlgorithm, ContentValues values) th @Override public int delete(String table, String whereClause, Object[] whereArgs) { - String[] args = new String[whereArgs.length]; - for(int index = 0; index < whereArgs.length; index++) { + int length = whereArgs == null ? 0 : whereArgs.length; + String[] args = new String[length]; + for(int index = 0; index < length; index++) { args[index] = whereArgs[index].toString(); } return delete(table, whereClause, args); @@ -1189,8 +1190,9 @@ public int delete(String table, String whereClause, Object[] whereArgs) { @Override public int update(String table, int conflictAlgorithm, ContentValues values, String whereClause, Object[] whereArgs) { - String[] args = new String[whereArgs.length]; - for(int index = 0; index < whereArgs.length; index++) { + int length = whereArgs == null ? 0 : whereArgs.length; + String[] args = new String[length]; + for(int index = 0; index < length; index++) { args[index] = whereArgs[index].toString(); } return updateWithOnConflict(table, values, whereClause, args, conflictAlgorithm); From 797ee61a351fcecd8dd5b75068193a12bacc0818 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Mon, 9 Sep 2024 10:12:38 -0500 Subject: [PATCH 07/25] Specify library version as undefined when not provided to Gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3300e32..96938f9 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ project.ext { if(project.hasProperty('sqlcipherAndroidVersion') && "${sqlcipherAndroidVersion}") { libraryVersion = "${sqlcipherAndroidVersion}" } else { - libraryVersion = "4.6.0" + libraryVersion = "undefined" } minSdkVersion = 21 androidXSQLiteVersion = "2.2.0" From 76a1db6b8d32a664f631607152c9ade086f9635c Mon Sep 17 00:00:00 2001 From: Stephen Lombardo Date: Wed, 30 Oct 2024 11:38:04 -0400 Subject: [PATCH 08/25] Excludes OpenSSL symbols from android libraries with --exclude-libs --- sqlcipher/src/main/jni/Application.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlcipher/src/main/jni/Application.mk b/sqlcipher/src/main/jni/Application.mk index 1ccba34..2a25aa5 100644 --- a/sqlcipher/src/main/jni/Application.mk +++ b/sqlcipher/src/main/jni/Application.mk @@ -1,2 +1,3 @@ APP_STL:=c++_static +APP_LDFLAGS += -Wl,--exclude-libs,ALL APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true From 0e1739baad53a34fdf13870986c87d94fdf3f742 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Fri, 15 Nov 2024 14:19:25 -0600 Subject: [PATCH 09/25] Remove test attempt to set value above 0xfffffffe Currently, SQLite prevents the max_page_count > 0xfffffffe --- .../zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java index e49bd08..a77d6d9 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java @@ -288,11 +288,6 @@ public void testAccessMaximumSize() { // the new maximum size is less than the current size. mDatabase.setMaximumSize(curMaximumSize - 1); assertEquals(curMaximumSize, mDatabase.getMaximumSize()); - - // the new maximum size is more than the current size. - mDatabase.setMaximumSize(curMaximumSize + 1); - assertEquals(curMaximumSize + mDatabase.getPageSize(), mDatabase.getMaximumSize()); - assertTrue(mDatabase.getMaximumSize() > curMaximumSize); } public void testAccessPageSize() { From 8166f309d3aa2763366eb52f7a1ea187bc226b02 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Fri, 15 Nov 2024 14:21:28 -0600 Subject: [PATCH 10/25] Update Gradle to 8.9, AGP to 8.7.2 --- Makefile | 6 +++++- README.md | 4 ++-- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- sqlcipher/build.gradle | 19 +++++++++++++++---- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 0eaa191..9ca7336 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,16 @@ .PHONY: clean build-debug build-release \ publish-snapshot-to-local-maven \ - publish-snapshot-to-local-nexus + publish-snapshot-to-local-nexus test GRADLE = ./gradlew clean: -rm -rf build $(GRADLE) clean +test: + ANDROID_SERIAL=$(shell adb devices | tail -n +2 | awk '!/emulator/{print $$1}') \ + $(GRADLE) :sqlcipher:connectedDebugAndroidTest + build-debug: $(GRADLE) assembleDebug diff --git a/README.md b/README.md index 77fda99..96cda3d 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ We welcome contributions, to contribute to SQLCipher for Android, a [contributor Add a local reference to the local library and dependency: ```groovy -implementation files('libs/sqlcipher-android-4.6.0-release.aar') +implementation files('libs/sqlcipher-android-undefined-release.aar') implementation 'androidx.sqlite:sqlite:2.2.0' ``` or source a Community edition build from Maven Central: ```groovy -implementation 'net.zetetic:sqlcipher-android:4.6.0@aar' +implementation 'net.zetetic:sqlcipher-android:undefined@aar' implementation 'androidx.sqlite:sqlite:2.2.0' ``` diff --git a/build.gradle b/build.gradle index 96938f9..c74042c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - def gradleToolsVersion = "7.4.1" + def gradleToolsVersion = "8.7.2" classpath "com.android.tools.build:gradle:${gradleToolsVersion}" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 93107ff..d76bd07 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 16 15:29:23 CDT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/sqlcipher/build.gradle b/sqlcipher/build.gradle index d79a5e4..3fff3d1 100644 --- a/sqlcipher/build.gradle +++ b/sqlcipher/build.gradle @@ -3,11 +3,11 @@ apply plugin: "maven-publish" apply plugin: "signing" android { - compileSdkVersion 33 - + compileSdkVersion 34 + namespace "net.zetetic.database" defaultConfig { minSdkVersion "${rootProject.ext.minSdkVersion}" - targetSdkVersion 33 + targetSdkVersion 34 versionCode 1 versionName "${rootProject.ext.libraryVersion}" project.archivesBaseName = "sqlcipher-android-${versionName}" @@ -55,6 +55,18 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + publishing { + singleVariant('release') { + withSourcesJar() + withJavadocJar() + } + + singleVariant('debug') { + withSourcesJar() + withJavadocJar() + } + } + } dependencies { @@ -80,7 +92,6 @@ allprojects { repositories { // The order in which you list these repositories matter. google() - jcenter() } } From f5d9335d8edd5de1e7c5d821c7bd7e953b635868 Mon Sep 17 00:00:00 2001 From: Stephen Lombardo Date: Mon, 18 Nov 2024 15:07:21 -0500 Subject: [PATCH 11/25] Allows the global connection pool size for WAL mode to be adjusted --- .../net/zetetic/database/sqlcipher/SQLiteGlobal.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteGlobal.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteGlobal.java index e030339..60e1cf9 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteGlobal.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteGlobal.java @@ -42,6 +42,7 @@ public final class SQLiteGlobal { private static final Object sLock = new Object(); private static int sDefaultPageSize = 4096; + private static int sWALConnectionPoolSize = 10; private static native int nativeReleaseMemory(); @@ -108,11 +109,18 @@ public static int getWALAutoCheckpoint() { return Math.max(1, value); } + /** + * Sets the connection pool size for WAL mode. + */ + public static void setWALConnectionPoolSize(int value) { + sWALConnectionPoolSize = value; + } + /** * Gets the connection pool size when in WAL mode. */ public static int getWALConnectionPoolSize() { - int value = 10; - return Math.max(2, value); + return sWALConnectionPoolSize; } + } From dfecce3dab562dd7a02ceffd4bfdc72b00866cbc Mon Sep 17 00:00:00 2001 From: Stephen Lombardo Date: Mon, 18 Nov 2024 15:17:40 -0500 Subject: [PATCH 12/25] Uses primary connection if available prior to using/creating secondary connections --- .../sqlcipher/SQLiteConnectionPool.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnectionPool.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnectionPool.java index 0750189..5f117fb 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnectionPool.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnectionPool.java @@ -617,15 +617,27 @@ private SQLiteConnection waitForConnection(String sql, int connectionFlags, // Try to acquire a connection. SQLiteConnection connection = null; - if (!wantPrimaryConnection) { - connection = tryAcquireNonPrimaryConnectionLocked( - sql, connectionFlags); // might throw - } - if (connection == null) { - connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw + + // When in WAL mode, we want to avoid the startup penalty of creating both a primary and + // non-primary connection for initial read operations on the database. If the pool of non-primary + // connections has not been initialized yet, and the primary connection is available, use the primary. + // If the primary connection is required, always use that. + if ((mAvailablePrimaryConnection != null && mAvailableNonPrimaryConnections.isEmpty()) || wantPrimaryConnection) { + connection = tryAcquirePrimaryConnectionLocked(connectionFlags); + if (connection != null) { + return connection; + } } - if (connection != null) { - return connection; + + // If a primary connection is not required by the caller, and either the non-primary connection pool + // has already been established or the primary connection is not available, then try to get + // non primary connection. This may establish a new non-primary connection a free one is not + // already available. + if (!wantPrimaryConnection) { + connection = tryAcquireNonPrimaryConnectionLocked(sql, connectionFlags); + if (connection != null) { + return connection; + } } // No connections available. Enqueue a waiter in priority order. From 786630b8bb6a0acf17f91c7b448631dfb048e27b Mon Sep 17 00:00:00 2001 From: RyuNen344 Date: Tue, 10 Dec 2024 01:55:11 +0900 Subject: [PATCH 13/25] chore(doc): corrected description of libcrypto.a location in README --- README.md | 4 ++-- README.md.template | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 96cda3d..c66cd20 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SQLCipher for Android provides a library replacement for `android.database.sqlit ### Compatibility -SQLCipher for Android supports Android API 21 and up on `armeabi-v7a`, `x86`, `x86_64`, and `arm64_v8a` architectures. +SQLCipher for Android supports Android API 21 and up on `armeabi-v7a`, `x86`, `x86_64`, and `arm64-v8a` architectures. ### Contributions @@ -90,7 +90,7 @@ This repository is not batteries-included. Specifically, you will need to build /sqlcipher/src/main/jni/sqlcipher/android-libs/armeabi-v7a/libcrypto.a /sqlcipher/src/main/jni/sqlcipher/android-libs/x86/libcrypto.a /sqlcipher/src/main/jni/sqlcipher/android-libs/x86_64/libcrypto.a -/sqlcipher/src/main/jni/sqlcipher/android-libs/arm64_v8a/libcrypto.a +/sqlcipher/src/main/jni/sqlcipher/android-libs/arm64-v8a/libcrypto.a /sqlcipher/src/main/jni/sqlcipher/android-libs/include/ /sqlcipher/src/main/jni/sqlcipher/sqlite3.c /sqlcipher/src/main/jni/sqlcipher/sqlite3.h diff --git a/README.md.template b/README.md.template index 1139295..84a3061 100644 --- a/README.md.template +++ b/README.md.template @@ -4,7 +4,7 @@ SQLCipher for Android provides a library replacement for `android.database.sqlit ### Compatibility -SQLCipher for Android supports Android API <%=minSdkVersion%> and up on `armeabi-v7a`, `x86`, `x86_64`, and `arm64_v8a` architectures. +SQLCipher for Android supports Android API <%=minSdkVersion%> and up on `armeabi-v7a`, `x86`, `x86_64`, and `arm64-v8a` architectures. ### Contributions @@ -90,7 +90,7 @@ This repository is not batteries-included. Specifically, you will need to build /sqlcipher/src/main/jni/sqlcipher/android-libs/armeabi-v7a/libcrypto.a /sqlcipher/src/main/jni/sqlcipher/android-libs/x86/libcrypto.a /sqlcipher/src/main/jni/sqlcipher/android-libs/x86_64/libcrypto.a -/sqlcipher/src/main/jni/sqlcipher/android-libs/arm64_v8a/libcrypto.a +/sqlcipher/src/main/jni/sqlcipher/android-libs/arm64-v8a/libcrypto.a /sqlcipher/src/main/jni/sqlcipher/android-libs/include/ /sqlcipher/src/main/jni/sqlcipher/sqlite3.c /sqlcipher/src/main/jni/sqlcipher/sqlite3.h From f6aca1318d5af0308bb9a7ff3a33b620726b2c8c Mon Sep 17 00:00:00 2001 From: Stephen Lombardo Date: Mon, 9 Dec 2024 17:15:55 -0500 Subject: [PATCH 14/25] Uses default Android CursorWindow size unless overridden by application --- .../database/sqlcipher/SQLiteCursor.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java index a04bbd9..63bf09a 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java @@ -42,9 +42,8 @@ public class SQLiteCursor extends AbstractWindowedCursor { static final String TAG = "SQLiteCursor"; static final int NO_COUNT = -1; - private static final int CURSOR_WINDOW_EXTRA = 512; private static boolean CURSOR_WINDOW_NEEDS_RECREATED = false; - private static final int DEFAULT_CURSOR_WINDOW_SIZE = (int)(8 * Math.pow(1024, 2)); + private static final int DEFAULT_CURSOR_WINDOW_SIZE = -1; public static int PREFERRED_CURSOR_WINDOW_SIZE = DEFAULT_CURSOR_WINDOW_SIZE; /** The name of the table to edit */ @@ -154,29 +153,38 @@ public static void resetCursorWindowSize() { ** versions are required. */ private void awc_clearOrCreateWindow(String name) { - int cursorWindowAllocationSize = PREFERRED_CURSOR_WINDOW_SIZE + CURSOR_WINDOW_EXTRA; + int cursorWindowAllocationSize = PREFERRED_CURSOR_WINDOW_SIZE; if (CURSOR_WINDOW_NEEDS_RECREATED) { awc_closeWindow(); CURSOR_WINDOW_NEEDS_RECREATED = false; } CursorWindow win = getWindow(); if ( win==null ) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - win = new CursorWindow(name, cursorWindowAllocationSize); - } else { - try { - @SuppressLint("DiscouragedPrivateApi") - Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize"); - if (field != null) { - field.setAccessible(true); - field.set(null, cursorWindowAllocationSize); - Log.i(TAG, String.format("Set CursorWindow allocation size to %s", cursorWindowAllocationSize)); + /* if the application has set a specific window size via setCursorWindowSize + * then use it. Otherwise, use the system default size defined internally + * by CursorWindow / com.android.internal.R.integer.config_cursorWindowSize + */ + if(cursorWindowAllocationSize > 0) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + win = new CursorWindow(name, cursorWindowAllocationSize); + } else { + try { + @SuppressLint("DiscouragedPrivateApi") + Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize"); + if (field != null) { + field.setAccessible(true); + field.set(null, cursorWindowAllocationSize); + Log.i(TAG, String.format("Set CursorWindow allocation size to %s", cursorWindowAllocationSize)); + } + } catch (Exception ex) { + Log.e(TAG, "Failed to override CursorWindow allocation size", ex); } - } catch (Exception ex) { - Log.e(TAG, "Failed to override CursorWindow allocation size", ex); + win = new CursorWindow(name); } + } else { win = new CursorWindow(name); } + setWindow(win); }else{ win.clear(); From 49dad2e0a5d9d4f427df6bb9d9882baac0ebbe8f Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Wed, 15 Jan 2025 15:23:48 -0600 Subject: [PATCH 15/25] Add native CursorWindow implementation with support dynamic resizing. Previously the JNI layer mapped query results back into a Java cursor window which was slow. Additionally, the previous implementation allocated an 8 MB backing buffer which was slow for smaller result sets. The new implementation by default will allocate a 16 kb buffer (plus extra for managing the linked list overhead), then grow to 2 MB (plus extra), then double in size as needed. Support for specifying a specific cursor window size is still supported via SQLiteCursor.setCursorWindowSize --- .../database_cts/AbstractCursorTest.java | 56 +- .../database_cts/CursorWindowTest.java | 59 +- .../sqlcipher_cts/RoomUpsertTest.java | 2 +- .../sqlcipher_cts/SQLCipherDatabaseTest.java | 82 ++- .../sqlcipher_cts/SQLiteCursorTest.java | 10 +- .../sqlcipher_cts/SupportAPIRoomTest.java | 7 +- .../net/zetetic/database/AbstractCursor.java | 425 ++++++++++++++ .../database/AbstractWindowedCursor.java | 177 ++++++ .../net/zetetic/database/CursorWindow.java | 512 +++++++++++++++++ .../CursorWindowAllocationException.java | 30 + .../net/zetetic/database/DatabaseUtils.java | 1 - .../net/zetetic/database/MatrixCursor.java | 297 ++++++++++ .../database/sqlcipher/SQLiteConnection.java | 8 +- .../database/sqlcipher/SQLiteCursor.java | 45 +- .../database/sqlcipher/SQLiteQuery.java | 3 +- .../database/sqlcipher/SQLiteSession.java | 7 +- sqlcipher/src/main/jni/sqlcipher/Android.mk | 8 +- .../src/main/jni/sqlcipher/CursorWindow.cpp | 289 ++++++++++ .../src/main/jni/sqlcipher/JNIString.cpp | 118 ++++ .../android_database_CursorWindow.cpp | 398 +++++++++++++ .../android_database_SQLiteCommon.cpp | 6 +- .../android_database_SQLiteConnection.cpp | 541 ++++++++---------- .../android_database_SQLiteDebug.cpp | 14 +- .../main/jni/sqlcipher/nativehelper/JNIHelp.h | 12 + 24 files changed, 2637 insertions(+), 470 deletions(-) create mode 100644 sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java create mode 100644 sqlcipher/src/main/java/net/zetetic/database/AbstractWindowedCursor.java create mode 100644 sqlcipher/src/main/java/net/zetetic/database/CursorWindow.java create mode 100644 sqlcipher/src/main/java/net/zetetic/database/CursorWindowAllocationException.java create mode 100644 sqlcipher/src/main/java/net/zetetic/database/MatrixCursor.java create mode 100644 sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp create mode 100644 sqlcipher/src/main/jni/sqlcipher/JNIString.cpp create mode 100644 sqlcipher/src/main/jni/sqlcipher/android_database_CursorWindow.cpp diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java index 4a849ee..f89a267 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java @@ -17,18 +17,21 @@ package net.zetetic.database.database_cts; import android.content.Context; -import android.database.AbstractCursor; import android.database.CharArrayBuffer; import android.database.ContentObserver; import android.database.CursorIndexOutOfBoundsException; -import android.database.CursorWindow; import android.database.DataSetObserver; +import net.zetetic.database.AbstractCursor; +import net.zetetic.database.CursorWindow; import net.zetetic.database.sqlcipher.SQLiteDatabase; + import android.net.Uri; import android.os.Bundle; import android.provider.Settings; import android.test.InstrumentationTestCase; +import androidx.test.filters.Suppress; + import java.io.File; import java.util.ArrayList; import java.util.Random; @@ -169,6 +172,7 @@ public void testOnChange() throws InterruptedException { assertTrue(mock.hadCalledOnChange()); } + @Suppress public void testOnMove() { assertFalse(mTestAbstractCursor.getOnMoveRet()); mTestAbstractCursor.moveToFirst(); @@ -182,6 +186,7 @@ public void testOnMove() { assertEquals(5, mTestAbstractCursor.getNewPos()); } + @Suppress public void testOnMove_samePosition() { mTestAbstractCursor.moveToFirst(); mTestAbstractCursor.moveToPosition(5); @@ -268,36 +273,10 @@ public void testIsClosed() { assertTrue(mDatabaseCursor.isClosed()); } - public void testGetWindow() { - CursorWindow window = new CursorWindow(false); - assertEquals(0, window.getNumRows()); - // fill window from position 0 - mDatabaseCursor.fillWindow(0, window); - - assertNotNull(mDatabaseCursor.getWindow()); - assertEquals(mDatabaseCursor.getCount(), window.getNumRows()); - - while (mDatabaseCursor.moveToNext()) { - assertEquals(mDatabaseCursor.getInt(POSITION1), - window.getInt(mDatabaseCursor.getPosition(), POSITION1)); - } - window.clear(); - } - public void testGetWantsAllOnMoveCalls() { assertFalse(mDatabaseCursor.getWantsAllOnMoveCalls()); } - public void testIsFieldUpdated() { - mTestAbstractCursor.moveToFirst(); - assertFalse(mTestAbstractCursor.isFieldUpdated(0)); - } - - public void testGetUpdatedField() { - mTestAbstractCursor.moveToFirst(); - assertNull(mTestAbstractCursor.getUpdatedField(0)); - } - public void testGetExtras() { assertSame(Bundle.EMPTY, mDatabaseCursor.getExtras()); } @@ -352,11 +331,12 @@ public void testDeactivate() { assertTrue(mock.hadCalledOnInvalid()); } + @Suppress public void testCopyStringToBuffer() { CharArrayBuffer ca = new CharArrayBuffer(1000); mTestAbstractCursor.moveToFirst(); mTestAbstractCursor.copyStringToBuffer(0, ca); - CursorWindow window = new CursorWindow(false); + CursorWindow window = new CursorWindow(""); mTestAbstractCursor.fillWindow(0, window); StringBuffer sb = new StringBuffer(); @@ -366,6 +346,7 @@ public void testCopyStringToBuffer() { assertEquals(sb.toString(), new String(ca.data, 0, ca.sizeCopied)); } + @Suppress public void testCheckPosition() { // Test with position = -1. try { @@ -502,7 +483,6 @@ public int getRowsMovedSum() { @Override public boolean onMove(int oldPosition, int newPosition) { - mOnMoveReturnValue = super.onMove(oldPosition, newPosition); mOldPosition = oldPosition; mNewPosition = newPosition; mRowsMovedSum += Math.abs(newPosition - oldPosition); @@ -560,26 +540,20 @@ public boolean isNull(int column) { return false; } + @Override + public int getType(int column) { + return 0; + } + public boolean hadCalledOnChange() { return mHadCalledOnChange; } - // the following are protected methods @Override protected void checkPosition() { super.checkPosition(); } - @Override - protected Object getUpdatedField(int columnIndex) { - return super.getUpdatedField(columnIndex); - } - - @Override - protected boolean isFieldUpdated(int columnIndex) { - return super.isFieldUpdated(columnIndex); - } - @Override protected void onChange(boolean selfChange) { super.onChange(selfChange); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java index 7b9cb87..055dee0 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java @@ -17,12 +17,14 @@ package net.zetetic.database.database_cts; import android.database.CharArrayBuffer; -import android.database.CursorWindow; -import android.database.MatrixCursor; import android.database.sqlite.SQLiteException; -import android.os.Parcel; import android.test.AndroidTestCase; +import net.zetetic.database.CursorWindow; +import net.zetetic.database.MatrixCursor; + +import org.junit.Before; + import java.util.ArrayList; import java.util.Arrays; import java.util.Random; @@ -31,6 +33,11 @@ public class CursorWindowTest extends AndroidTestCase { private static final String TEST_STRING = "Test String"; + @Before + public void setUp() { + System.loadLibrary("sqlcipher"); + } + public void testWriteCursorToWindow() throws Exception { // create cursor String[] colNames = new String[]{"_id", "name", "number", "profit"}; @@ -42,7 +49,7 @@ public void testWriteCursorToWindow() throws Exception { } // fill window - CursorWindow window = new CursorWindow(false); + CursorWindow window = new CursorWindow(""); cursor.fillWindow(0, window); // read from cursor window @@ -97,34 +104,13 @@ public void testEmptyString() { } public void testConstructors() { - int TEST_NUMBER = 5; - CursorWindow cursorWindow; - // Test constructor with 'true' input, and getStartPosition should return 0 - cursorWindow = new CursorWindow(true); - assertEquals(0, cursorWindow.getStartPosition()); - - // Test constructor with 'false' input - cursorWindow = new CursorWindow(false); + CursorWindow cursorWindow = new CursorWindow(""); assertEquals(0, cursorWindow.getStartPosition()); - - // Test newFromParcel - Parcel parcel = Parcel.obtain(); - cursorWindow = new CursorWindow(true); - cursorWindow.setStartPosition(TEST_NUMBER); - cursorWindow.setNumColumns(1); - cursorWindow.allocRow(); - cursorWindow.putString(TEST_STRING, TEST_NUMBER, 0); - cursorWindow.writeToParcel(parcel, 0); - - parcel.setDataPosition(0); - cursorWindow = CursorWindow.CREATOR.createFromParcel(parcel); - assertEquals(TEST_NUMBER, cursorWindow.getStartPosition()); - assertEquals(TEST_STRING, cursorWindow.getString(TEST_NUMBER, 0)); } public void testDataStructureOperations() { - CursorWindow cursorWindow = new CursorWindow(true); + CursorWindow cursorWindow = new CursorWindow(""); // Test with normal values assertTrue(cursorWindow.setNumColumns(0)); @@ -145,7 +131,7 @@ public void testDataStructureOperations() { cursorWindow.freeLastRow(); assertEquals(0, cursorWindow.getNumRows()); - cursorWindow = new CursorWindow(true); + cursorWindow = new CursorWindow(""); assertTrue(cursorWindow.setNumColumns(6)); assertTrue(cursorWindow.allocRow()); // Column number set to negative number, so now can put values. @@ -190,7 +176,7 @@ public void testAccessDataValues() { originalBlob[i] = (byte) i; } - CursorWindow cursorWindow = new CursorWindow(true); + CursorWindow cursorWindow = new CursorWindow(""); cursorWindow.setNumColumns(5); cursorWindow.allocRow(); @@ -280,7 +266,7 @@ public void testAccessDataValues() { } public void testCopyStringToBuffer() { - int DEFAULT_ARRAY_LENGTH = 64; + int DEFAULT_ARRAY_LENGTH = 60; String baseString = "0123456789"; String expectedString = ""; // Create a 60 characters string. @@ -288,7 +274,7 @@ public void testCopyStringToBuffer() { expectedString += baseString; } CharArrayBuffer charArrayBuffer = new CharArrayBuffer(null); - CursorWindow cursorWindow = new CursorWindow(true); + CursorWindow cursorWindow = new CursorWindow(""); cursorWindow.setNumColumns(2); cursorWindow.allocRow(); @@ -318,7 +304,7 @@ public void testAccessStartPosition() { final int TEST_POSITION_1 = 0; final int TEST_POSITION_2 = 3; - CursorWindow cursorWindow = new CursorWindow(true); + CursorWindow cursorWindow = new CursorWindow(""); fillCursorTestContents(cursorWindow, 5); // Test setStartPosition @@ -369,16 +355,11 @@ public void testClearAndOnAllReferencesReleased() { assertTrue(cursorWindow.hasReleasedAllReferences()); } - public void testDescribeContents() { - CursorWindow cursorWindow = new CursorWindow(true); - assertEquals(0, cursorWindow.describeContents()); - } - private class MockCursorWindow extends CursorWindow { private boolean mHasReleasedAllReferences = false; public MockCursorWindow(boolean localWindow) { - super(localWindow); + super(""); } @Override @@ -426,7 +407,7 @@ private static ArrayList> createTestList(int rows, int cols) * The method comes from unit_test CursorWindowTest. */ private CursorWindow getOneByOneWindow() { - CursorWindow window = new CursorWindow(false); + CursorWindow window = new CursorWindow(""); assertTrue(window.setNumColumns(1)); assertTrue(window.allocRow()); return window; diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/RoomUpsertTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/RoomUpsertTest.java index e495521..a2d6fca 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/RoomUpsertTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/RoomUpsertTest.java @@ -72,7 +72,7 @@ public static abstract class UserDao { abstract User[] findById(long id); } - @Database(entities = {User.class}, version = 1) + @Database(entities = {User.class}, version = 1, exportSchema = false) public static abstract class UserDatabase extends RoomDatabase { abstract UserDao userDao(); } diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java index eff3175..68f8a1d 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java @@ -2,14 +2,13 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; import static org.junit.Assert.fail; import android.content.ContentValues; import android.database.Cursor; -import android.database.sqlite.SQLiteDatabaseCorruptException; import android.database.sqlite.SQLiteException; +import android.util.Log; import net.zetetic.database.sqlcipher.SQLiteCursor; import net.zetetic.database.sqlcipher.SQLiteDatabase; @@ -21,6 +20,8 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.HashMap; +import java.util.UUID; public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { @@ -477,29 +478,78 @@ public void shouldRetrieveLargeSingleRowResultFromCursor(){ } @Test - public void shouldAllowCursorWindowToResize(){ + public void shouldAllowCursorWindowToResizeWithNonDefaultAllocationSize(){ try { Cursor cursor; int id = 1, extra = 1024, size = 256; - byte[] tooLargeQueriedData = null; SQLiteCursor.setCursorWindowSize(size); byte[] tooLargeData = generateRandomBytes(size + extra); database.execSQL("create table t1(a,b);"); database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{id, tooLargeData}); - try { - cursor = database.rawQuery("select b from t1 where a = ?;", new Object[]{id}); - if(cursor != null && cursor.moveToFirst()) { - fail("CursorWindow should be too small to fill query results"); - } - } catch (Exception ex){ - SQLiteCursor.setCursorWindowSize(size + extra); - cursor = database.rawQuery("select b from t1 where a = ?;", new Object[]{id}); - if(cursor != null && cursor.moveToFirst()) { - tooLargeQueriedData = cursor.getBlob(0); - cursor.close(); + cursor = database.rawQuery("select b from t1 where a = ?;", id); + if (cursor != null && cursor.moveToFirst()) { + byte[] value = cursor.getBlob(0); + assertThat(Arrays.equals(value, tooLargeData), is(true)); + } else { + fail("CursorWindow should resize automatically to fill query results"); + } + } finally { + SQLiteCursor.resetCursorWindowSize(); + } + } + + @Test + public void shouldTestSmallResizeOfAllocationForCursor(){ + try { + Cursor cursor; + int id = 1, size = 1024 * 3000; + SQLiteCursor.resetCursorWindowSize(); + byte[] tooLargeData = generateRandomBytes(size); + database.execSQL("create table t1(a,b);"); + database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{id, tooLargeData}); + long start = System.nanoTime(); + cursor = database.rawQuery("select b from t1 where a = ?;", id); + if (cursor != null && cursor.moveToFirst()) { + byte[] value = cursor.getBlob(0); + assertThat(Arrays.equals(value, tooLargeData), is(true)); + } else { + fail("CursorWindow should resize automatically to fill query results"); + } + long stop = System.nanoTime(); + Log.i(TAG, String.format("Query completed in %d ms", (stop - start)/1000000)); + } finally { + SQLiteCursor.resetCursorWindowSize(); + } + } + + @Test + public void shouldAllowCursorWindowToResizeWithDefaultAllocationSize(){ + try { + Cursor cursor; + int dataRows = 10, size = 2048 * 1024; + SQLiteCursor.resetCursorWindowSize(); + HashMap data = new HashMap<>(); + for (int row = 0; row < dataRows; row++) { + data.put(UUID.randomUUID().toString(), generateRandomBytes(size)); + } + database.execSQL("create table t1(a,b);"); + for (String key : data.keySet()) { + database.execSQL("insert into t1(a,b) values(?,?);", new Object[]{key, data.get(key)}); + } + long start = System.nanoTime(); + cursor = database.rawQuery("select * from t1;"); + if(cursor != null){ + while(cursor.moveToNext()){ + String key = cursor.getString(0); + byte[] value = cursor.getBlob(1); + assertThat(Arrays.equals(data.get(key), value), is(true)); } + long stop = System.nanoTime(); + Log.i(TAG, String.format("Query completed in %d ms", (stop - start)/1000000)); + cursor.close(); + } else { + fail("Unable to retrieve data from database"); } - assertThat(Arrays.equals(tooLargeQueriedData, tooLargeData), is(true)); } finally { SQLiteCursor.resetCursorWindowSize(); } diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java index 919e558..5601a5f 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java @@ -17,13 +17,13 @@ package net.zetetic.database.sqlcipher_cts; -import android.database.AbstractCursor; import android.database.Cursor; -import android.database.CursorWindow; import android.database.DataSetObserver; import android.database.StaleDataException; import android.test.AndroidTestCase; +import net.zetetic.database.AbstractCursor; +import net.zetetic.database.CursorWindow; import net.zetetic.database.sqlcipher.SQLiteCursor; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDirectCursorDriver; @@ -94,7 +94,7 @@ public void testClose() { public void testRegisterDataSetObserver() { SQLiteCursor cursor = getCursor(); - MockCursorWindow cursorWindow = new MockCursorWindow(false); + MockCursorWindow cursorWindow = new MockCursorWindow(); MockObserver observer = new MockObserver(); @@ -271,8 +271,8 @@ protected boolean hasInvalidated () { private class MockCursorWindow extends CursorWindow { private boolean mIsClosed = false; - public MockCursorWindow(boolean localWindow) { - super(localWindow); + public MockCursorWindow() { + super(""); } @Override diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportAPIRoomTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportAPIRoomTest.java index 9acb9f0..0e7895c 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportAPIRoomTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SupportAPIRoomTest.java @@ -1,7 +1,6 @@ package net.zetetic.database.sqlcipher_cts; import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; @@ -29,7 +28,6 @@ import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,6 +48,9 @@ public void before(){ Context context = ApplicationProvider.getApplicationContext(); System.loadLibrary("sqlcipher"); databaseFile = context.getDatabasePath("users.db"); + if(databaseFile.exists()){ + databaseFile.delete(); + } SupportOpenHelperFactory factory = new SupportOpenHelperFactory("user".getBytes(StandardCharsets.UTF_8)); db = Room.databaseBuilder(context, AppDatabase.class, databaseFile.getAbsolutePath()) .openHelperFactory(factory).build(); @@ -117,7 +118,7 @@ public void after(){ } } - @Database(entities = {User.class}, version = 1) + @Database(entities = {User.class}, version = 1, exportSchema = false) public abstract static class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } diff --git a/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java b/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java new file mode 100644 index 0000000..b953229 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// modified from original source see README at the top level of this project + +package net.zetetic.database; + +import android.content.ContentResolver; +import android.database.CharArrayBuffer; +import android.database.ContentObservable; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.CursorIndexOutOfBoundsException; +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +import java.lang.ref.WeakReference; + +/** + * This is an abstract cursor class that handles a lot of the common code + * that all cursors need to deal with and is provided for convenience reasons. + */ +public abstract class AbstractCursor implements Cursor { + + private static final String TAG = "Cursor"; + + protected int mPos; + + protected boolean mClosed; + + //@Deprecated // deprecated in AOSP but still used for non-deprecated methods + protected ContentResolver mContentResolver; + + private Uri mNotifyUri; + + private final Object mSelfObserverLock = new Object(); + private ContentObserver mSelfObserver; + private boolean mSelfObserverRegistered; + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + private final ContentObservable mContentObservable = new ContentObservable(); + + private Bundle mExtras = Bundle.EMPTY; + + public void fillWindow(int position, CursorWindow window) { + DatabaseUtils.cursorFillWindow(this, position, window); + } + + @Override + abstract public int getCount(); + + @Override + abstract public String[] getColumnNames(); + + @Override + abstract public String getString(int column); + @Override + abstract public short getShort(int column); + @Override + abstract public int getInt(int column); + @Override + abstract public long getLong(int column); + @Override + abstract public float getFloat(int column); + @Override + abstract public double getDouble(int column); + @Override + abstract public boolean isNull(int column); + + @Override + public abstract int getType(int column); + + @Override + public byte[] getBlob(int column) { + throw new UnsupportedOperationException("getBlob is not supported"); + } + + @Override + public int getColumnCount() { + return getColumnNames().length; + } + + @Override + public void deactivate() { + onDeactivateOrClose(); + } + + /** @hide */ + protected void onDeactivateOrClose() { + if (mSelfObserver != null) { + mContentResolver.unregisterContentObserver(mSelfObserver); + mSelfObserverRegistered = false; + } + mDataSetObservable.notifyInvalidated(); + } + + @Override + public boolean requery() { + if (mSelfObserver != null && !mSelfObserverRegistered) { + mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); + mSelfObserverRegistered = true; + } + mDataSetObservable.notifyChanged(); + return true; + } + + @Override + public boolean isClosed() { + return mClosed; + } + + @Override + public void close() { + mClosed = true; + mContentObservable.unregisterAll(); + onDeactivateOrClose(); + } + + @Override + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + // Default implementation, uses getString + String result = getString(columnIndex); + if (result != null) { + char[] data = buffer.data; + if (data == null || data.length < result.length()) { + buffer.data = result.toCharArray(); + } else { + result.getChars(0, result.length(), data, 0); + } + buffer.sizeCopied = result.length(); + } else { + buffer.sizeCopied = 0; + } + } + + public AbstractCursor() { + mPos = -1; + } + + @Override + public final int getPosition() { + return mPos; + } + + @Override + public final boolean moveToPosition(int position) { + // Make sure position isn't past the end of the cursor + final int count = getCount(); + if (position >= count) { + mPos = count; + return false; + } + + // Make sure position isn't before the beginning of the cursor + if (position < 0) { + mPos = -1; + return false; + } + + // Check for no-op moves, and skip the rest of the work for them + if (position == mPos) { + return true; + } + + boolean result = onMove(mPos, position); + if (!result) { + mPos = -1; + } else { + mPos = position; + } + + return result; + } + + /** + * This function is called every time the cursor is successfully scrolled + * to a new position, giving the subclass a chance to update any state it + * may have. If it returns false the move function will also do so and the + * cursor will scroll to the beforeFirst position. + *

+ * This function should be called by methods such as {@link #moveToPosition(int)}, + * so it will typically not be called from outside of the cursor class itself. + *

+ * + * @param oldPosition The position that we're moving from. + * @param newPosition The position that we're moving to. + * @return True if the move is successful, false otherwise. + */ + public abstract boolean onMove(int oldPosition, int newPosition); + + @Override + public final boolean move(int offset) { + return moveToPosition(mPos + offset); + } + + @Override + public final boolean moveToFirst() { + return moveToPosition(0); + } + + @Override + public final boolean moveToLast() { + return moveToPosition(getCount() - 1); + } + + @Override + public final boolean moveToNext() { + return moveToPosition(mPos + 1); + } + + @Override + public final boolean moveToPrevious() { + return moveToPosition(mPos - 1); + } + + @Override + public final boolean isFirst() { + return mPos == 0 && getCount() != 0; + } + + @Override + public final boolean isLast() { + int cnt = getCount(); + return mPos == (cnt - 1) && cnt != 0; + } + + @Override + public final boolean isBeforeFirst() { + return getCount() == 0 || mPos == -1; + } + + @Override + public final boolean isAfterLast() { + return getCount() == 0 || mPos == getCount(); + } + + @Override + public int getColumnIndex(String columnName) { + // Hack according to bug 903852 + final int periodIndex = columnName.lastIndexOf('.'); + if (periodIndex != -1) { + Exception e = new Exception(); + Log.e(TAG, "requesting column name with table name -- " + columnName, e); + columnName = columnName.substring(periodIndex + 1); + } + + String columnNames[] = getColumnNames(); + int length = columnNames.length; + for (int i = 0; i < length; i++) { + if (columnNames[i].equalsIgnoreCase(columnName)) { + return i; + } + } + return -1; + } + + @Override + public int getColumnIndexOrThrow(String columnName) { + final int index = getColumnIndex(columnName); + if (index < 0) { + throw new IllegalArgumentException("column '" + columnName + "' does not exist"); + } + return index; + } + + @Override + public String getColumnName(int columnIndex) { + return getColumnNames()[columnIndex]; + } + + @Override + public void registerContentObserver(ContentObserver observer) { + mContentObservable.registerObserver(observer); + } + + @Override + public void unregisterContentObserver(ContentObserver observer) { + // cursor will unregister all observers when it close + if (!mClosed) { + mContentObservable.unregisterObserver(observer); + } + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + } + + /** + * Subclasses must call this method when they finish committing updates to notify all + * observers. + * + * @param selfChange value + */ + @SuppressWarnings("deprecation") + protected void onChange(boolean selfChange) { + synchronized (mSelfObserverLock) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mContentObservable.dispatchChange(selfChange, null); + } else { + mContentObservable.dispatchChange(selfChange); + } + if (mNotifyUri != null && selfChange) { + mContentResolver.notifyChange(mNotifyUri, mSelfObserver); + } + } + } + + /** + * Specifies a content URI to watch for changes. + * + * @param cr The content resolver from the caller's context. + * @param notifyUri The URI to watch for changes. This can be a + * specific row URI, or a base URI for a whole class of content. + */ + @Override + public void setNotificationUri(ContentResolver cr, Uri notifyUri) { + synchronized (mSelfObserverLock) { + mNotifyUri = notifyUri; + mContentResolver = cr; + if (mSelfObserver != null) { + mContentResolver.unregisterContentObserver(mSelfObserver); + } + mSelfObserver = new SelfContentObserver(this); + mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); + mSelfObserverRegistered = true; + } + } + + @Override + public Uri getNotificationUri() { + synchronized (mSelfObserverLock) { + return mNotifyUri; + } + } + + @Override + public boolean getWantsAllOnMoveCalls() { + return false; + } + + @Override + public void setExtras(Bundle extras) { + mExtras = (extras == null) ? Bundle.EMPTY : extras; + } + + @Override + public Bundle getExtras() { + return mExtras; + } + + @Override + public Bundle respond(Bundle extras) { + return Bundle.EMPTY; + } + + /** + * This function throws CursorIndexOutOfBoundsException if the cursor position is out of bounds. + * Subclass implementations of the get functions should call this before attempting to + * retrieve data. + * + * @throws CursorIndexOutOfBoundsException + */ + protected void checkPosition() { + if (-1 == mPos || getCount() == mPos) { + throw new CursorIndexOutOfBoundsException(mPos, getCount()); + } + } + + @SuppressWarnings("FinalizeDoesntCallSuperFinalize") + @Override + protected void finalize() { + if (mSelfObserver != null && mSelfObserverRegistered) { + mContentResolver.unregisterContentObserver(mSelfObserver); + } + try { + if (!mClosed) close(); + } catch(Exception ignored) { } + } + + /** + * Cursors use this class to track changes others make to their URI. + */ + protected static class SelfContentObserver extends ContentObserver { + WeakReference mCursor; + + public SelfContentObserver(AbstractCursor cursor) { + super(null); + mCursor = new WeakReference<>(cursor); + } + + @Override + public boolean deliverSelfNotifications() { + return false; + } + + @Override + public void onChange(boolean selfChange) { + AbstractCursor cursor = mCursor.get(); + if (cursor != null) { + cursor.onChange(false); + } + } + } +} diff --git a/sqlcipher/src/main/java/net/zetetic/database/AbstractWindowedCursor.java b/sqlcipher/src/main/java/net/zetetic/database/AbstractWindowedCursor.java new file mode 100644 index 0000000..7c5cace --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/AbstractWindowedCursor.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// modified from original source see README at the top level of this project + +package net.zetetic.database; + +import android.database.CharArrayBuffer; +import android.database.Cursor; +import android.database.StaleDataException; + +/** + * A base class for Cursors that store their data in {@link android.database.CursorWindow}s. + *

+ * The cursor owns the cursor window it uses. When the cursor is closed, + * its window is also closed. Likewise, when the window used by the cursor is + * changed, its old window is closed. This policy of strict ownership ensures + * that cursor windows are not leaked. + *

+ * Subclasses are responsible for filling the cursor window with data during + * {@link #onMove(int, int)}, allocating a new cursor window if necessary. + * During {@link #requery()}, the existing cursor window should be cleared and + * filled with new data. + *

+ * If the contents of the cursor change or become invalid, the old window must be closed + * (because it is owned by the cursor) and set to null. + *

+ */ +@SuppressWarnings("unused") +public abstract class AbstractWindowedCursor extends AbstractCursor { + /** + * The cursor window owned by this cursor. + */ + protected CursorWindow mWindow; + + @Override + public byte[] getBlob(int columnIndex) { + checkPosition(); + return mWindow.getBlob(mPos, columnIndex); + } + + @Override + public String getString(int columnIndex) { + checkPosition(); + return mWindow.getString(mPos, columnIndex); + } + + @Override + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + mWindow.copyStringToBuffer(mPos, columnIndex, buffer); + } + + @Override + public short getShort(int columnIndex) { + checkPosition(); + return mWindow.getShort(mPos, columnIndex); + } + + @Override + public int getInt(int columnIndex) { + checkPosition(); + return mWindow.getInt(mPos, columnIndex); + } + + @Override + public long getLong(int columnIndex) { + checkPosition(); + return mWindow.getLong(mPos, columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + checkPosition(); + return mWindow.getFloat(mPos, columnIndex); + } + + @Override + public double getDouble(int columnIndex) { + checkPosition(); + return mWindow.getDouble(mPos, columnIndex); + } + + @Override + public boolean isNull(int columnIndex) { + return mWindow.getType(mPos, columnIndex) == Cursor.FIELD_TYPE_NULL; + } + + @Override + public int getType(int columnIndex) { + return mWindow.getType(mPos, columnIndex); + } + + @Override + protected void checkPosition() { + super.checkPosition(); + if (mWindow == null) { + throw new StaleDataException("Attempting to access a closed CursorWindow." + + "Most probable cause: cursor is deactivated prior to calling this method."); + } + } + + public CursorWindow getWindow() { + return mWindow; + } + + /** + * Sets a new cursor window for the cursor to use. + *

+ * The cursor takes ownership of the provided cursor window; the cursor window + * will be closed when the cursor is closed or when the cursor adopts a new + * cursor window. + *

+ * If the cursor previously had a cursor window, then it is closed when the + * new cursor window is assigned. + *

+ * + * @param window The new cursor window, typically a remote cursor window. + */ + public void setWindow(CursorWindow window) { + if (window != mWindow) { + closeWindow(); + mWindow = window; + } + } + + /** + * Returns true if the cursor has an associated cursor window. + * + * @return True if the cursor has an associated cursor window. + */ + public boolean hasWindow() { + return mWindow != null; + } + + /** + * Closes the cursor window and sets {@link #mWindow} to null. + * @hide + */ + protected void closeWindow() { + if (mWindow != null) { + mWindow.close(); + mWindow = null; + } + } + + /** + * If there is a window, clear it. Otherwise, creates a new window. + * + * @param name The window name. + * @hide + */ + protected void clearOrCreateWindow(String name) { + if (mWindow == null) { + mWindow = new CursorWindow(name); + } else { + mWindow.clear(); + } + } + + @Override + protected void onDeactivateOrClose() { + super.onDeactivateOrClose(); + closeWindow(); + } +} \ No newline at end of file diff --git a/sqlcipher/src/main/java/net/zetetic/database/CursorWindow.java b/sqlcipher/src/main/java/net/zetetic/database/CursorWindow.java new file mode 100644 index 0000000..a604305 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/CursorWindow.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// modified from original source see README at the top level of this project + +package net.zetetic.database; + +import android.database.CharArrayBuffer; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; + +import net.zetetic.database.sqlcipher.SQLiteClosable; + +/** + * A buffer containing multiple cursor rows. + */ +@SuppressWarnings("unused") +public class CursorWindow extends SQLiteClosable { + + private static final int WINDOW_SIZE_KB = 16; + public static int PREFERRED_CURSOR_WINDOW_SIZE = WINDOW_SIZE_KB * 1024; + public static final int DEFAULT_CURSOR_WINDOW_SIZE = WINDOW_SIZE_KB * 1024; + private final int mWindowSizeBytes; + + /** + * The native CursorWindow object pointer. (FOR INTERNAL USE ONLY) + */ + public long mWindowPtr; + + private int mStartPos; + private final String mName; + + private static native long nativeCreate(String name, int cursorWindowSize); + private static native void nativeDispose(long windowPtr); + + private static native void nativeClear(long windowPtr); + + private static native int nativeGetNumRows(long windowPtr); + private static native boolean nativeSetNumColumns(long windowPtr, int columnNum); + private static native boolean nativeAllocRow(long windowPtr); + private static native void nativeFreeLastRow(long windowPtr); + + private static native int nativeGetType(long windowPtr, int row, int column); + private static native byte[] nativeGetBlob(long windowPtr, int row, int column); + private static native String nativeGetString(long windowPtr, int row, int column); + private static native long nativeGetLong(long windowPtr, int row, int column); + private static native double nativeGetDouble(long windowPtr, int row, int column); + + private static native boolean nativePutBlob(long windowPtr, byte[] value, int row, int column); + private static native boolean nativePutString(long windowPtr, String value, int row, int column); + private static native boolean nativePutLong(long windowPtr, long value, int row, int column); + private static native boolean nativePutDouble(long windowPtr, double value, int row, int column); + private static native boolean nativePutNull(long windowPtr, int row, int column); + + private static native String nativeGetName(long windowPtr); + + /** + * Creates a new empty cursor with default cursor size (currently 2MB) + */ + public CursorWindow(String name) { + this(name, DEFAULT_CURSOR_WINDOW_SIZE); + } + + /** + * Creates a new empty cursor window and gives it a name. + *

+ * The cursor initially has no rows or columns. Call {@link #setNumColumns(int)} to + * set the number of columns before adding any rows to the cursor. + *

+ * + * @param name The name of the cursor window, or null if none. + * @param windowSizeBytes Size of cursor window in bytes. + * + * Note: Memory is dynamically allocated as data rows are added to + * the window. Depending on the amount of data stored, the actual + * amount of memory allocated can be lower than specified size, + * but cannot exceed it. Value is a non-negative number of bytes. + */ + public CursorWindow(String name, int windowSizeBytes) { + /* In + https://developer.android.com/reference/android/database/CursorWindow#CursorWindow(java.lang.String,%20long) + windowSizeBytes is long. However windowSizeBytes is + eventually transformed into a size_t in cpp, and I can not + guarantee that long->size_t would be possible. I thus keep + int. This means that we can create cursor of size up to 4GiB + while upstream can theoretically create cursor of size up to + 16 EiB. It is probably an acceptable restriction.*/ + mStartPos = 0; + mWindowSizeBytes = windowSizeBytes; + mName = name != null && name.length() != 0 ? name : ""; + mWindowPtr = nativeCreate(mName, windowSizeBytes); + if (mWindowPtr == 0) { + throw new CursorWindowAllocationException("Cursor window allocation of " + + (windowSizeBytes / 1024) + " kb failed. "); + } + } + + @SuppressWarnings("ThrowFromFinallyBlock") + @Override + protected void finalize() throws Throwable { + try { + dispose(); + } finally { + super.finalize(); + } + } + + private void dispose() { + if (mWindowPtr != 0) { + nativeDispose(mWindowPtr); + mWindowPtr = 0; + } + } + + /** + * Gets the name of this cursor window, never null. + */ + public String getName() { + return mName; + } + + /** + * Clears out the existing contents of the window, making it safe to reuse + * for new data. + *

+ * The start position ({@link #getStartPosition()}), number of rows ({@link #getNumRows()}), + * and number of columns in the cursor are all reset to zero. + *

+ */ + public void clear() { + mStartPos = 0; + nativeClear(mWindowPtr); + } + + /** + * Gets the start position of this cursor window. + *

+ * The start position is the zero-based index of the first row that this window contains + * relative to the entire result set of the {@link Cursor}. + *

+ * + * @return The zero-based start position. + */ + public int getStartPosition() { + return mStartPos; + } + + /** + * Sets the start position of this cursor window. + *

+ * The start position is the zero-based index of the first row that this window contains + * relative to the entire result set of the {@link Cursor}. + *

+ * + * @param pos The new zero-based start position. + */ + public void setStartPosition(int pos) { + mStartPos = pos; + } + + /** + * Gets the number of rows in this window. + * + * @return The number of rows in this cursor window. + */ + public int getNumRows() { + return nativeGetNumRows(mWindowPtr); + } + + /** + * Sets the number of columns in this window. + *

+ * This method must be called before any rows are added to the window, otherwise + * it will fail to set the number of columns if it differs from the current number + * of columns. + *

+ * + * @param columnNum The new number of columns. + * @return True if successful. + */ + public boolean setNumColumns(int columnNum) { + return nativeSetNumColumns(mWindowPtr, columnNum); + } + + /** + * Allocates a new row at the end of this cursor window. + * + * @return True if successful, false if the cursor window is out of memory. + */ + public boolean allocRow(){ + return nativeAllocRow(mWindowPtr); + } + + /** + * Frees the last row in this cursor window. + */ + public void freeLastRow(){ + nativeFreeLastRow(mWindowPtr); + } + + /** + * Returns the type of the field at the specified row and column index. + *

+ * The returned field types are: + *

    + *
  • {@link Cursor#FIELD_TYPE_NULL}
  • + *
  • {@link Cursor#FIELD_TYPE_INTEGER}
  • + *
  • {@link Cursor#FIELD_TYPE_FLOAT}
  • + *
  • {@link Cursor#FIELD_TYPE_STRING}
  • + *
  • {@link Cursor#FIELD_TYPE_BLOB}
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The field type. + */ + public int getType(int row, int column) { + return nativeGetType(mWindowPtr, row - mStartPos, column); + } + + /** + * Gets the value of the field at the specified row and column index as a byte array. + *

+ * The result is determined as follows: + *

    + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result + * is null.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then the result + * is the blob value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result + * is the array of bytes that make up the internal representation of the + * string value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER} or + * {@link Cursor#FIELD_TYPE_FLOAT}, then a {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a byte array. + */ + public byte[] getBlob(int row, int column) { + return nativeGetBlob(mWindowPtr, row - mStartPos, column); + } + + /** + * Gets the value of the field at the specified row and column index as a string. + *

+ * The result is determined as follows: + *

    + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result + * is null.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result + * is the string value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result + * is a string representation of the integer in decimal, obtained by formatting the + * value with the printf family of functions using + * format specifier %lld.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result + * is a string representation of the floating-point value in decimal, obtained by + * formatting the value with the printf family of functions using + * format specifier %g.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a + * {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a string. + */ + public String getString(int row, int column) { + return nativeGetString(mWindowPtr, row - mStartPos, column); + } + + /** + * Copies the text of the field at the specified row and column index into + * a {@link CharArrayBuffer}. + *

+ * The buffer is populated as follows: + *

    + *
  • If the buffer is too small for the value to be copied, then it is + * automatically resized.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the buffer + * is set to an empty string.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the buffer + * is set to the contents of the string.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the buffer + * is set to a string representation of the integer in decimal, obtained by formatting the + * value with the printf family of functions using + * format specifier %lld.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the buffer is + * set to a string representation of the floating-point value in decimal, obtained by + * formatting the value with the printf family of functions using + * format specifier %g.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a + * {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @param buffer The {@link CharArrayBuffer} to hold the string. It is automatically + * resized if the requested string is larger than the buffer's current capacity. + */ + public void copyStringToBuffer(int row, int column, CharArrayBuffer buffer) { + if (buffer == null) { + throw new IllegalArgumentException("CharArrayBuffer should not be null"); + } + // TODO not as optimal as the original code + char[] chars = getString(row, column).toCharArray(); + buffer.data = chars; + buffer.sizeCopied = chars.length; + } + + /** + * Gets the value of the field at the specified row and column index as a long. + *

+ * The result is determined as follows: + *

    + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result + * is 0L.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result + * is the value obtained by parsing the string value with strtoll. + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result + * is the long value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result + * is the floating-point value converted to a long.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a + * {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a long. + */ + public long getLong(int row, int column) { + return nativeGetLong(mWindowPtr, row - mStartPos, column); + } + + /** + * Gets the value of the field at the specified row and column index as a + * double. + *

+ * The result is determined as follows: + *

    + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result + * is 0.0.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result + * is the value obtained by parsing the string value with strtod. + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result + * is the integer value converted to a double.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result + * is the double value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a + * {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a double. + */ + public double getDouble(int row, int column) { + return nativeGetDouble(mWindowPtr, row - mStartPos, column); + } + + /** + * Gets the value of the field at the specified row and column index as a + * short. + *

+ * The result is determined by invoking {@link #getLong} and converting the + * result to short. + *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a short. + */ + public short getShort(int row, int column) { + return (short) getLong(row, column); + } + + /** + * Gets the value of the field at the specified row and column index as an + * int. + *

+ * The result is determined by invoking {@link #getLong} and converting the + * result to int. + *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as an int. + */ + public int getInt(int row, int column) { + return (int) getLong(row, column); + } + + /** + * Gets the value of the field at the specified row and column index as a + * float. + *

+ * The result is determined by invoking {@link #getDouble} and converting the + * result to float. + *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as an float. + */ + public float getFloat(int row, int column) { + return (float) getDouble(row, column); + } + + /** + * Copies a byte array into the field at the specified row and column index. + * + * @param value The value to store. + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putBlob(byte[] value, int row, int column) { + return nativePutBlob(mWindowPtr, value, row - mStartPos, column); + } + + /** + * Copies a string into the field at the specified row and column index. + * + * @param value The value to store. + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putString(String value, int row, int column) { + return nativePutString(mWindowPtr, value, row - mStartPos, column); + } + + /** + * Puts a long integer into the field at the specified row and column index. + * + * @param value The value to store. + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putLong(long value, int row, int column) { + return nativePutLong(mWindowPtr, value, row - mStartPos, column); + } + + /** + * Puts a double-precision floating point value into the field at the + * specified row and column index. + * + * @param value The value to store. + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putDouble(double value, int row, int column) { + return nativePutDouble(mWindowPtr, value, row - mStartPos, column); + } + + /** + * Puts a null value into the field at the specified row and column index. + * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putNull(int row, int column) { + return nativePutNull(mWindowPtr, row - mStartPos, column); + } + + public boolean isNull(int row, int column) { + return getType(row, column) == Cursor.FIELD_TYPE_NULL; + } + + public boolean isBlob(int row, int column) { + int type = getType(row, column); + return type == Cursor.FIELD_TYPE_BLOB || type == Cursor.FIELD_TYPE_NULL; + } + + @Override + protected void onAllReferencesReleased() { + dispose(); + } + + @Override + public String toString() { + return getName() + " {" + Long.toHexString(mWindowPtr) + "}"; + } + + public int getWindowSizeBytes() { + return mWindowSizeBytes; + } +} diff --git a/sqlcipher/src/main/java/net/zetetic/database/CursorWindowAllocationException.java b/sqlcipher/src/main/java/net/zetetic/database/CursorWindowAllocationException.java new file mode 100644 index 0000000..7c2baef --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/CursorWindowAllocationException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.zetetic.database; + +/** + * This exception is thrown when a CursorWindow couldn't be allocated, + * most probably due to memory not being available. + * + * @hide + */ +public class CursorWindowAllocationException extends RuntimeException { + public CursorWindowAllocationException(String description) { + super(description); + } +} + diff --git a/sqlcipher/src/main/java/net/zetetic/database/DatabaseUtils.java b/sqlcipher/src/main/java/net/zetetic/database/DatabaseUtils.java index 41358ae..ae81357 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/DatabaseUtils.java +++ b/sqlcipher/src/main/java/net/zetetic/database/DatabaseUtils.java @@ -24,7 +24,6 @@ import net.zetetic.database.sqlcipher.SQLiteProgram; import net.zetetic.database.sqlcipher.SQLiteStatement; -import android.database.CursorWindow; import android.database.SQLException; import android.database.sqlite.SQLiteAbortException; import android.database.sqlite.SQLiteConstraintException; diff --git a/sqlcipher/src/main/java/net/zetetic/database/MatrixCursor.java b/sqlcipher/src/main/java/net/zetetic/database/MatrixCursor.java new file mode 100644 index 0000000..0048692 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/MatrixCursor.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.zetetic.database; + +import android.database.CursorIndexOutOfBoundsException; + +import java.util.ArrayList; + + +/** + * A mutable cursor implementation backed by an array of {@code Object}s. Use + * {@link #newRow()} to add rows. Automatically expands internal capacity + * as needed. + */ +public class MatrixCursor extends AbstractCursor { + + private final String[] columnNames; + private Object[] data; + private int rowCount = 0; + private final int columnCount; + + /** + * Constructs a new cursor with the given initial capacity. + * + * @param columnNames names of the columns, the ordering of which + * determines column ordering elsewhere in this cursor + * @param initialCapacity in rows + */ + public MatrixCursor(String[] columnNames, int initialCapacity) { + this.columnNames = columnNames; + this.columnCount = columnNames.length; + + if (initialCapacity < 1) { + initialCapacity = 1; + } + + this.data = new Object[columnCount * initialCapacity]; + } + + /** + * Constructs a new cursor. + * + * @param columnNames names of the columns, the ordering of which + * determines column ordering elsewhere in this cursor + */ + public MatrixCursor(String[] columnNames) { + this(columnNames, 16); + } + + /** + * Gets value at the given column for the current row. + */ + private Object get(int column) { + if (column < 0 || column >= columnCount) { + throw new CursorIndexOutOfBoundsException("Requested column: " + + column + ", # of columns: " + columnCount); + } + if (mPos < 0) { + throw new CursorIndexOutOfBoundsException("Before first row."); + } + if (mPos >= rowCount) { + throw new CursorIndexOutOfBoundsException("After last row."); + } + return data[mPos * columnCount + column]; + } + + /** + * Adds a new row to the end and returns a builder for that row. Not safe + * for concurrent use. + * + * @return builder which can be used to set the column values for the new + * row + */ + public RowBuilder newRow() { + rowCount++; + int endIndex = rowCount * columnCount; + ensureCapacity(endIndex); + int start = endIndex - columnCount; + return new RowBuilder(start, endIndex); + } + + /** + * Adds a new row to the end with the given column values. Not safe + * for concurrent use. + * + * @throws IllegalArgumentException if {@code columnValues.length != + * columnNames.length} + * @param columnValues in the same order as the the column names specified + * at cursor construction time + */ + public void addRow(Object[] columnValues) { + if (columnValues.length != columnCount) { + throw new IllegalArgumentException("columnNames.length = " + + columnCount + ", columnValues.length = " + + columnValues.length); + } + + int start = rowCount++ * columnCount; + ensureCapacity(start + columnCount); + System.arraycopy(columnValues, 0, data, start, columnCount); + } + + /** + * Adds a new row to the end with the given column values. Not safe + * for concurrent use. + * + * @throws IllegalArgumentException if {@code columnValues.size() != + * columnNames.length} + * @param columnValues in the same order as the the column names specified + * at cursor construction time + */ + public void addRow(Iterable columnValues) { + int start = rowCount * columnCount; + int end = start + columnCount; + ensureCapacity(end); + + if (columnValues instanceof ArrayList) { + addRow((ArrayList) columnValues, start); + return; + } + + int current = start; + Object[] localData = data; + for (Object columnValue : columnValues) { + if (current == end) { + // TODO: null out row? + throw new IllegalArgumentException( + "columnValues.size() > columnNames.length"); + } + localData[current++] = columnValue; + } + + if (current != end) { + // TODO: null out row? + throw new IllegalArgumentException( + "columnValues.size() < columnNames.length"); + } + + // Increase row count here in case we encounter an exception. + rowCount++; + } + + /** Optimization for {@link ArrayList}. */ + private void addRow(ArrayList columnValues, int start) { + int size = columnValues.size(); + if (size != columnCount) { + throw new IllegalArgumentException("columnNames.length = " + + columnCount + ", columnValues.size() = " + size); + } + + rowCount++; + Object[] localData = data; + for (int i = 0; i < size; i++) { + localData[start + i] = columnValues.get(i); + } + } + + /** Ensures that this cursor has enough capacity. */ + private void ensureCapacity(int size) { + if (size > data.length) { + Object[] oldData = this.data; + int newSize = data.length * 2; + if (newSize < size) { + newSize = size; + } + this.data = new Object[newSize]; + System.arraycopy(oldData, 0, this.data, 0, oldData.length); + } + } + + public void fillWindow(int position, CursorWindow window) { + DatabaseUtils.cursorFillWindow(this, position, window); + } + + /** + * Builds a row, starting from the left-most column and adding one column + * value at a time. Follows the same ordering as the column names specified + * at cursor construction time. + */ + public class RowBuilder { + + private int index; + private final int endIndex; + + RowBuilder(int index, int endIndex) { + this.index = index; + this.endIndex = endIndex; + } + + /** + * Sets the next column value in this row. + * + * @throws CursorIndexOutOfBoundsException if you try to add too many + * values + * @return this builder to support chaining + */ + public RowBuilder add(Object columnValue) { + if (index == endIndex) { + throw new CursorIndexOutOfBoundsException( + "No more columns left."); + } + + data[index++] = columnValue; + return this; + } + } + + // AbstractCursor implementation. + + @Override + public int getCount() { + return rowCount; + } + + @Override + public String[] getColumnNames() { + return columnNames; + } + + @Override + public String getString(int column) { + Object value = get(column); + if (value == null) return null; + return value.toString(); + } + + @Override + public short getShort(int column) { + Object value = get(column); + if (value == null) return 0; + if (value instanceof Number) return ((Number) value).shortValue(); + return Short.parseShort(value.toString()); + } + + @Override + public int getInt(int column) { + Object value = get(column); + if (value == null) return 0; + if (value instanceof Number) return ((Number) value).intValue(); + return Integer.parseInt(value.toString()); + } + + @Override + public long getLong(int column) { + Object value = get(column); + if (value == null) return 0; + if (value instanceof Number) return ((Number) value).longValue(); + return Long.parseLong(value.toString()); + } + + @Override + public float getFloat(int column) { + Object value = get(column); + if (value == null) return 0.0f; + if (value instanceof Number) return ((Number) value).floatValue(); + return Float.parseFloat(value.toString()); + } + + @Override + public double getDouble(int column) { + Object value = get(column); + if (value == null) return 0.0d; + if (value instanceof Number) return ((Number) value).doubleValue(); + return Double.parseDouble(value.toString()); + } + + @Override + public int getType(int column) { + return DatabaseUtils.getTypeOfObject(get(column)); + } + + @Override + public boolean onMove(int oldPosition, int newPosition) { + return true; + } + + @Override + public boolean isNull(int column) { + return get(column) == null; + } + + +} \ No newline at end of file diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnection.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnection.java index e17f6b4..f2da861 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnection.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnection.java @@ -21,7 +21,6 @@ package net.zetetic.database.sqlcipher; import android.database.Cursor; -import android.database.CursorWindow; import android.database.sqlite.SQLiteBindOrColumnIndexOutOfRangeException; import android.database.sqlite.SQLiteDatabaseLockedException; import android.database.sqlite.SQLiteException; @@ -33,6 +32,7 @@ import android.util.LruCache; import android.util.Printer; +import net.zetetic.database.CursorWindow; import net.zetetic.database.DatabaseUtils; import net.zetetic.database.sqlcipher.SQLiteDebug.DbStats; @@ -157,8 +157,8 @@ private static native int nativeExecuteForBlobFileDescriptor( private static native long nativeExecuteForLastInsertedRowId( long connectionPtr, long statementPtr); private static native long nativeExecuteForCursorWindow( - long connectionPtr, long statementPtr, CursorWindow win, - int startPos, int requiredPos, boolean countAllRows); + long connectionPtr, long statementPtr, long winPtr, + int startPos, int requiredPos, boolean countAllRows); private static native int nativeGetDbLookaside(long connectionPtr); private static native void nativeCancel(long connectionPtr); private static native void nativeResetCancel(long connectionPtr, boolean cancelable); @@ -927,7 +927,7 @@ public int executeForCursorWindow(String sql, Object[] bindArgs, attachCancellationSignal(cancellationSignal); try { final long result = nativeExecuteForCursorWindow( - mConnectionPtr, statement.mStatementPtr, window, + mConnectionPtr, statement.mStatementPtr, window.mWindowPtr, startPos, requiredPos, countAllRows); actualPos = (int)(result >> 32); countedRows = (int)result; diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java index 63bf09a..595e701 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java @@ -20,15 +20,12 @@ package net.zetetic.database.sqlcipher; -import android.annotation.SuppressLint; -import android.database.AbstractWindowedCursor; -import android.database.CursorWindow; +import net.zetetic.database.AbstractWindowedCursor; +import net.zetetic.database.CursorWindow; import net.zetetic.database.DatabaseUtils; -import android.os.Build; import android.util.Log; -import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; @@ -137,54 +134,24 @@ public int getCount() { } public static void setCursorWindowSize(int size) { - PREFERRED_CURSOR_WINDOW_SIZE = size; + CursorWindow.PREFERRED_CURSOR_WINDOW_SIZE = size; CURSOR_WINDOW_NEEDS_RECREATED = true; } public static void resetCursorWindowSize() { - PREFERRED_CURSOR_WINDOW_SIZE = DEFAULT_CURSOR_WINDOW_SIZE; + CursorWindow.PREFERRED_CURSOR_WINDOW_SIZE = CursorWindow.DEFAULT_CURSOR_WINDOW_SIZE; CURSOR_WINDOW_NEEDS_RECREATED = true; } - /* - ** The AbstractWindowClass contains protected methods clearOrCreateWindow() and - ** closeWindow(), which are used by the android.database.sqlite.* version of this - ** class. But, since they are marked with "@hide", the following replacement - ** versions are required. - */ private void awc_clearOrCreateWindow(String name) { - int cursorWindowAllocationSize = PREFERRED_CURSOR_WINDOW_SIZE; + int cursorWindowAllocationSize = CursorWindow.PREFERRED_CURSOR_WINDOW_SIZE; if (CURSOR_WINDOW_NEEDS_RECREATED) { awc_closeWindow(); CURSOR_WINDOW_NEEDS_RECREATED = false; } CursorWindow win = getWindow(); if ( win==null ) { - /* if the application has set a specific window size via setCursorWindowSize - * then use it. Otherwise, use the system default size defined internally - * by CursorWindow / com.android.internal.R.integer.config_cursorWindowSize - */ - if(cursorWindowAllocationSize > 0) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - win = new CursorWindow(name, cursorWindowAllocationSize); - } else { - try { - @SuppressLint("DiscouragedPrivateApi") - Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize"); - if (field != null) { - field.setAccessible(true); - field.set(null, cursorWindowAllocationSize); - Log.i(TAG, String.format("Set CursorWindow allocation size to %s", cursorWindowAllocationSize)); - } - } catch (Exception ex) { - Log.e(TAG, "Failed to override CursorWindow allocation size", ex); - } - win = new CursorWindow(name); - } - } else { - win = new CursorWindow(name); - } - + win = new CursorWindow(name, cursorWindowAllocationSize); setWindow(win); }else{ win.clear(); diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java index f81ac7b..7ff8a61 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java @@ -20,13 +20,14 @@ package net.zetetic.database.sqlcipher; -import android.database.CursorWindow; import android.database.sqlite.SQLiteDatabaseCorruptException; import android.database.sqlite.SQLiteException; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.util.Log; +import net.zetetic.database.CursorWindow; + /** * Represents a query that reads the resulting rows into a {@link SQLiteQuery}. * This class is used by {@link SQLiteCursor} and isn't useful itself. diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteSession.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteSession.java index d25e405..d1f1a5f 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteSession.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteSession.java @@ -20,12 +20,13 @@ package net.zetetic.database.sqlcipher; -import android.database.CursorWindow; import android.database.DatabaseUtils; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; +import net.zetetic.database.CursorWindow; + /** * Provides a single client the ability to use a database. * @@ -852,8 +853,8 @@ public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int conne * @throws OperationCanceledException if the operation was canceled. */ public int executeForCursorWindow(String sql, Object[] bindArgs, - CursorWindow window, int startPos, int requiredPos, boolean countAllRows, - int connectionFlags, CancellationSignal cancellationSignal) { + CursorWindow window, int startPos, int requiredPos, boolean countAllRows, + int connectionFlags, CancellationSignal cancellationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } diff --git a/sqlcipher/src/main/jni/sqlcipher/Android.mk b/sqlcipher/src/main/jni/sqlcipher/Android.mk index b9b46e1..6ed942b 100644 --- a/sqlcipher/src/main/jni/sqlcipher/Android.mk +++ b/sqlcipher/src/main/jni/sqlcipher/Android.mk @@ -19,12 +19,16 @@ LOCAL_CPPFLAGS += -Wno-conversion-null $(info SQLCipher LOCAL_CFLAGS:${LOCAL_CFLAGS}) -LOCAL_SRC_FILES:= \ +LOCAL_SRC_FILES:= \ android_database_SQLiteCommon.cpp \ android_database_SQLiteConnection.cpp \ + android_database_CursorWindow.cpp \ android_database_SQLiteGlobal.cpp \ android_database_SQLiteDebug.cpp \ - JNIHelp.cpp JniConstants.cpp + JNIHelp.cpp \ + JniConstants.cpp \ + JNIString.cpp \ + CursorWindow.cpp LOCAL_SRC_FILES += sqlite3.c diff --git a/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp new file mode 100644 index 0000000..2ead43f --- /dev/null +++ b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2006-2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// modified from original source see README at the top level of this project + +#undef LOG_TAG +#define LOG_TAG "CursorWindow" + +#include "CursorWindow.h" +#include "ALog-priv.h" + +#include +#include +#include + +namespace android { + + CursorWindow::CursorWindow(const char* name, void* data, size_t size, bool readOnly) : + mData(data), mSize(size), mReadOnly(readOnly) { + mName = strdup(name); + mHeader = static_cast(mData); + } + + CursorWindow::~CursorWindow() { + free(mName); + free(mData); + } + + status_t CursorWindow::create(const char* name, size_t size, CursorWindow** outWindow) { + status_t result; + size_t requestedSize = size + CURSOR_SIZE_EXTRA; + void* data = malloc(requestedSize); + if (!data) { + return NO_MEMORY; + } + auto* window = new CursorWindow(name, data, requestedSize, false); + result = window->clear(); + if (!result) { + ALOGD("Created new CursorWindow: freeOffset=%d, " + "numRows=%d, numColumns=%d, mSize=%zu, mData=%p", + window->mHeader->freeOffset, + window->mHeader->numRows, + window->mHeader->numColumns, + window->mSize, window->mData); + *outWindow = window; + return OK; + } + delete window; + return result; + } + + status_t CursorWindow::clear() { + if (mReadOnly) { + return INVALID_OPERATION; + } + mHeader->freeOffset = sizeof(Header) + sizeof(RowSlotChunk); + mHeader->firstChunkOffset = sizeof(Header); + mHeader->numRows = 0; + mHeader->numColumns = 0; + auto* firstChunk = static_cast(offsetToPtr(mHeader->firstChunkOffset)); + firstChunk->nextChunkOffset = 0; + return OK; + } + + status_t CursorWindow::setNumColumns(uint32_t numColumns) { + if (mReadOnly) { + return INVALID_OPERATION; + } + uint32_t cur = mHeader->numColumns; + if ((cur > 0 || mHeader->numRows > 0) && cur != numColumns) { + ALOGE("Trying to go from %d columns to %d", cur, numColumns); + return INVALID_OPERATION; + } + mHeader->numColumns = numColumns; + return OK; + } + + status_t CursorWindow::allocRow() { + if (mReadOnly) { + return INVALID_OPERATION; + } + // Fill in the row slot + RowSlot* rowSlot = allocRowSlot(); + if (rowSlot == nullptr) { + return NO_MEMORY; + } + // Allocate the slots for the field directory + size_t fieldDirSize = mHeader->numColumns * sizeof(FieldSlot); + uint32_t fieldDirOffset = alloc(fieldDirSize, true /*aligned*/); + if (!fieldDirOffset) { + mHeader->numRows--; + ALOGD("The row failed, so back out the new row accounting " + "from allocRowSlot %d", mHeader->numRows); + return NO_MEMORY; + } + auto* fieldDir = static_cast(offsetToPtr(fieldDirOffset)); + memset(fieldDir, 0, fieldDirSize); + rowSlot->offset = fieldDirOffset; + return OK; + } + + status_t CursorWindow::freeLastRow() { + if (mReadOnly) { + return INVALID_OPERATION; + } + if (mHeader->numRows > 0) { + mHeader->numRows--; + } + return OK; + } + + status_t CursorWindow::maybeInflate() { + size_t initialSize = 16 * 1024 + CURSOR_SIZE_EXTRA; + size_t newSize = mSize <= initialSize + ? 2048 * 1024 + CURSOR_SIZE_EXTRA + : mSize * 2 + CURSOR_SIZE_EXTRA; + uint32_t freeOffset = mHeader->freeOffset; + ALOGW("Request to resize CursorWindow allocation: current window size %zu bytes, " + "free space %zu bytes, new window size %zu bytes", + mSize, freeSpace(), newSize); + if((mData = realloc(mData, newSize))){ + mHeader = static_cast(mData); + mHeader->freeOffset = freeOffset; + mSize = newSize; + ALOGW("Resized CursorWindow allocation: current window size %zu bytes", + newSize); + return OK; + } else { + ALOGE("Failed to resize CursorWindow allocation"); + return NO_MEMORY; + } + } + + uint32_t CursorWindow::alloc(size_t size, bool aligned) { // NOLINT(*-no-recursion) + uint32_t padding; + if (aligned) { + // 4 byte alignment + padding = (~mHeader->freeOffset + 1) & 3; + } else { + padding = 0; + } + uint32_t offset = mHeader->freeOffset + padding; + uint32_t nextFreeOffset = offset + size; + if (nextFreeOffset > mSize) { + status_t result = maybeInflate(); + if(result == OK){ + return alloc(size, aligned); + } + ALOGW("Window is full: requested allocation %zu bytes, " + "free space %zu bytes, window size %zu bytes", + size, freeSpace(), mSize); + return 0; + } + mHeader->freeOffset = nextFreeOffset; + return offset; + } + + CursorWindow::RowSlot* CursorWindow::getRowSlot(uint32_t row) { + uint32_t chunkPos = row; + auto* chunk = static_cast( + offsetToPtr(mHeader->firstChunkOffset)); + while (chunkPos >= ROW_SLOT_CHUNK_NUM_ROWS) { + chunk = static_cast(offsetToPtr(chunk->nextChunkOffset)); + chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS; + } + return &chunk->slots[chunkPos]; + } + + CursorWindow::RowSlot* CursorWindow::allocRowSlot() { + uint32_t chunkPos = mHeader->numRows; + auto* chunk = static_cast( + offsetToPtr(mHeader->firstChunkOffset)); + while (chunkPos > ROW_SLOT_CHUNK_NUM_ROWS) { + chunk = static_cast(offsetToPtr(chunk->nextChunkOffset)); + chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS; + } + if (chunkPos == ROW_SLOT_CHUNK_NUM_ROWS) { + if (!chunk->nextChunkOffset) { + chunk->nextChunkOffset = alloc(sizeof(RowSlotChunk), true /*aligned*/); + if (!chunk->nextChunkOffset) { + return nullptr; + } + } + chunk = static_cast(offsetToPtr(chunk->nextChunkOffset)); + chunk->nextChunkOffset = 0; + chunkPos = 0; + } + mHeader->numRows += 1; + return &chunk->slots[chunkPos]; + } + + CursorWindow::FieldSlot* CursorWindow::getFieldSlot(uint32_t row, uint32_t column) { + if (row >= mHeader->numRows || column >= mHeader->numColumns) { + ALOGE("Failed to read row %d, column %d from a CursorWindow which " + "has %d rows, %d columns.", + row, column, mHeader->numRows, mHeader->numColumns); + return nullptr; + } + RowSlot* rowSlot = getRowSlot(row); + if (!rowSlot) { + ALOGE("Failed to find rowSlot for row %d.", row); + return nullptr; + } + auto* fieldDir = static_cast(offsetToPtr(rowSlot->offset)); + return &fieldDir[column]; + } + + status_t CursorWindow::putBlob(uint32_t row, uint32_t column, const void* value, size_t size) { + return putBlobOrString(row, column, value, size, FIELD_TYPE_BLOB); + } + + status_t CursorWindow::putString(uint32_t row, uint32_t column, const char* value, + size_t sizeIncludingNull) { + return putBlobOrString(row, column, value, sizeIncludingNull, FIELD_TYPE_STRING); + } + + status_t CursorWindow::putBlobOrString(uint32_t row, uint32_t column, + const void* value, size_t size, int32_t type) { + if (mReadOnly) { + return INVALID_OPERATION; + } + uint32_t offset = alloc(size); + if (!offset) { + return NO_MEMORY; + } + memcpy(offsetToPtr(offset), value, size); + FieldSlot* fieldSlot = getFieldSlot(row, column); + if (!fieldSlot) { + return BAD_VALUE; + } + fieldSlot->type = type; + fieldSlot->data.buffer.offset = offset; + fieldSlot->data.buffer.size = size; + return OK; + } + + status_t CursorWindow::putLong(uint32_t row, uint32_t column, int64_t value) { + if (mReadOnly) { + return INVALID_OPERATION; + } + FieldSlot* fieldSlot = getFieldSlot(row, column); + if (!fieldSlot) { + return BAD_VALUE; + } + fieldSlot->type = FIELD_TYPE_INTEGER; + fieldSlot->data.l = value; + return OK; + } + + status_t CursorWindow::putDouble(uint32_t row, uint32_t column, double value) { + if (mReadOnly) { + return INVALID_OPERATION; + } + FieldSlot* fieldSlot = getFieldSlot(row, column); + if (!fieldSlot) { + return BAD_VALUE; + } + fieldSlot->type = FIELD_TYPE_FLOAT; + fieldSlot->data.d = value; + return OK; + } + + status_t CursorWindow::putNull(uint32_t row, uint32_t column) { + if (mReadOnly) { + return INVALID_OPERATION; + } + FieldSlot* fieldSlot = getFieldSlot(row, column); + if (!fieldSlot) { + return BAD_VALUE; + } + fieldSlot->type = FIELD_TYPE_NULL; + fieldSlot->data.buffer.offset = 0; + fieldSlot->data.buffer.size = 0; + return OK; + } + +} // namespace android diff --git a/sqlcipher/src/main/jni/sqlcipher/JNIString.cpp b/sqlcipher/src/main/jni/sqlcipher/JNIString.cpp new file mode 100644 index 0000000..7841156 --- /dev/null +++ b/sqlcipher/src/main/jni/sqlcipher/JNIString.cpp @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Note this code is adapted from AOSP implementation of String, now located at +// https://android.googlesource.com/platform/libcore/+/master/libart/src/main/java/java/lang/StringFactory.java + +#include + +#define REPLACEMENT_CHAR 0xfffd; + +namespace android { + + jsize utf8ToJavaCharArray(const char* d, jchar v[], jint byteCount) { + jint idx = 0; + jint last = byteCount; + jint s = 0; + outer: + while (idx < last) { + jbyte b0 = d[idx++]; + if ((b0 & 0x80) == 0) { + // 0xxxxxxx + // Range: U-00000000 - U-0000007F + jint val = b0 & 0xff; + v[s++] = (jchar) val; + } else if (((b0 & 0xe0) == 0xc0) || ((b0 & 0xf0) == 0xe0) || + ((b0 & 0xf8) == 0xf0) || ((b0 & 0xfc) == 0xf8) || ((b0 & 0xfe) == 0xfc)) { + jint utfCount = 1; + if ((b0 & 0xf0) == 0xe0) utfCount = 2; + else if ((b0 & 0xf8) == 0xf0) utfCount = 3; + else if ((b0 & 0xfc) == 0xf8) utfCount = 4; + else if ((b0 & 0xfe) == 0xfc) utfCount = 5; + + // 110xxxxx (10xxxxxx)+ + // Range: U-00000080 - U-000007FF (count == 1) + // Range: U-00000800 - U-0000FFFF (count == 2) + // Range: U-00010000 - U-001FFFFF (count == 3) + // Range: U-00200000 - U-03FFFFFF (count == 4) + // Range: U-04000000 - U-7FFFFFFF (count == 5) + + if (idx + utfCount > last) { + v[s++] = REPLACEMENT_CHAR; + continue; + } + + // Extract usable bits from b0 + jint val = b0 & (0x1f >> (utfCount - 1)); + for (int i = 0; i < utfCount; ++i) { + jbyte b = d[idx++]; + if ((b & 0xc0) != 0x80) { + v[s++] = REPLACEMENT_CHAR; + idx--; // Put the input char back + goto outer; + } + // Push new bits in from the right side + val <<= 6; + val |= b & 0x3f; + } + + // Note: Java allows overlong char + // specifications To disallow, check that val + // is greater than or equal to the minimum + // value for each count: + // + // count min value + // ----- ---------- + // 1 0x80 + // 2 0x800 + // 3 0x10000 + // 4 0x200000 + // 5 0x4000000 + + // Allow surrogate values (0xD800 - 0xDFFF) to + // be specified using 3-byte UTF values only + if ((utfCount != 2) && (val >= 0xD800) && (val <= 0xDFFF)) { + v[s++] = REPLACEMENT_CHAR; + continue; + } + + // Reject chars greater than the Unicode maximum of U+10FFFF. + if (val > 0x10FFFF) { + v[s++] = REPLACEMENT_CHAR; + continue; + } + + // Encode chars from U+10000 up as surrogate pairs + if (val < 0x10000) { + v[s++] = (jchar) val; + } else { + int x = val & 0xffff; + int u = (val >> 16) & 0x1f; + int w = (u - 1) & 0xffff; + int hi = 0xd800 | (w << 6) | (x >> 10); + int lo = 0xdc00 | (x & 0x3ff); + v[s++] = (jchar) hi; + v[s++] = (jchar) lo; + } + } else { + // Illegal values 0x8*, 0x9*, 0xa*, 0xb*, 0xfd-0xff + v[s++] = REPLACEMENT_CHAR; + } + } + return s; + } +} \ No newline at end of file diff --git a/sqlcipher/src/main/jni/sqlcipher/android_database_CursorWindow.cpp b/sqlcipher/src/main/jni/sqlcipher/android_database_CursorWindow.cpp new file mode 100644 index 0000000..a835f6d --- /dev/null +++ b/sqlcipher/src/main/jni/sqlcipher/android_database_CursorWindow.cpp @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// modified from original source see README at the top level of this project + +#undef LOG_TAG +#define LOG_TAG "CursorWindow" +#define __STDC_FORMAT_MACROS + +#include +#include +#include +#include +#include + +#include "JNIHelp.h" +#include "CursorWindow.h" +#include "android_database_SQLiteCommon.h" + +namespace android { + + static struct { + jfieldID data; + jfieldID sizeCopied; + } gCharArrayBufferClassInfo; + + static jstring gEmptyString = nullptr; + + static void throwExceptionWithCol(JNIEnv* env, jint column) { + char buf[64]; + snprintf(buf, sizeof(buf), "Couldn't set column %d", column); + jniThrowException(env, "java/lang/IllegalStateException", buf); + } + + static void throwExceptionWithRowCol(JNIEnv* env, jint row, jint column) { + char buf[64]; + snprintf(buf, sizeof(buf), "Couldn't read row %d column %d", row, column); + jniThrowException(env, "java/lang/IllegalStateException", buf); + } + + static void throwUnknownTypeException(JNIEnv * env, jint type) { + char buf[32]; + snprintf(buf, sizeof(buf), "UNKNOWN type %d", type); + jniThrowException(env, "java/lang/IllegalStateException", buf); + } + + static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring nameObj, jint cursorWindowSize) { + CursorWindow* window; + const char* nameStr = env->GetStringUTFChars(nameObj, nullptr); + status_t status = CursorWindow::create(nameStr, cursorWindowSize, &window); + env->ReleaseStringUTFChars(nameObj, nameStr); + + if (status || !window) { + ALOGE("Could not allocate CursorWindow of size %d due to error %d.", + cursorWindowSize, status); + return 0; + } + + ALOGD("nativeInitializeEmpty: window = %p", window); + return reinterpret_cast(window); + } + + static void nativeDispose(JNIEnv* env, jclass clazz, jlong windowPtr) { + auto* window = reinterpret_cast(windowPtr); + if (window) { + ALOGD("Closing window %p", window); + delete window; + } + } + + static jstring nativeGetName(JNIEnv* env, jclass clazz, jlong windowPtr) { + auto* window = reinterpret_cast(windowPtr); + return env->NewStringUTF(window->name()); + } + + static void nativeClear(JNIEnv * env, jclass clazz, jlong windowPtr) { + auto* window = reinterpret_cast(windowPtr); + ALOGD("Clearing window %p", window); + status_t status = window->clear(); + if (status) { + ALOGD("Could not clear window. error=%d", status); + } + } + + static jint nativeGetNumRows(JNIEnv* env, jclass clazz, jlong windowPtr) { + auto* window = reinterpret_cast(windowPtr); + return window->getNumRows(); + } + + static jboolean nativeSetNumColumns(JNIEnv* env, jclass clazz, jlong windowPtr, + jint columnNum) { + auto* window = reinterpret_cast(windowPtr); + if(window == nullptr){ + throwExceptionWithCol(env, columnNum); + return false; + } + status_t status = window->setNumColumns(columnNum); + return status == OK; + } + + static jboolean nativeAllocRow(JNIEnv* env, jclass clazz, jlong windowPtr) { + auto* window = reinterpret_cast(windowPtr); + status_t status = window->allocRow(); + return status == OK; + } + + static void nativeFreeLastRow(JNIEnv* env, jclass clazz, jlong windowPtr) { + auto* window = reinterpret_cast(windowPtr); + window->freeLastRow(); + } + + static jint nativeGetType(JNIEnv* env, jclass clazz, jlong windowPtr, + jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + ALOGD("returning column type affinity for %d,%d from %p", row, column, window); + CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column); + if (!fieldSlot) { + return CursorWindow::FIELD_TYPE_NULL; + } + return android::CursorWindow::getFieldSlotType(fieldSlot); + } + + static jbyteArray nativeGetBlob(JNIEnv* env, jclass clazz, jlong windowPtr, + jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column); + if (!fieldSlot) { + throwExceptionWithRowCol(env, row, column); + return nullptr; + } + int32_t type = android::CursorWindow::getFieldSlotType(fieldSlot); + if (type == CursorWindow::FIELD_TYPE_BLOB || type == CursorWindow::FIELD_TYPE_STRING) { + size_t size; + const void* value = window->getFieldSlotValueBlob(fieldSlot, &size); + jbyteArray byteArray = env->NewByteArray(size); + if (!byteArray) { + env->ExceptionClear(); + throw_sqlite3_exception(env, "Native could not create new byte[]"); + return NULL; + } + env->SetByteArrayRegion(byteArray, 0, size, static_cast(value)); + return byteArray; + } else if (type == CursorWindow::FIELD_TYPE_INTEGER) { + throw_sqlite3_exception(env, "INTEGER data in nativeGetBlob "); + } else if (type == CursorWindow::FIELD_TYPE_FLOAT) { + throw_sqlite3_exception(env, "FLOAT data in nativeGetBlob "); + } else if (type == CursorWindow::FIELD_TYPE_NULL) { + // do nothing + } else { + throwUnknownTypeException(env, type); + } + return nullptr; + } + + extern int utf8ToJavaCharArray(const char* d, jchar v[], jint byteCount); + + static jstring nativeGetString(JNIEnv* env, jclass clazz, jlong windowPtr, + jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column); + if (!fieldSlot) { + throwExceptionWithRowCol(env, row, column); + return nullptr; + } + int32_t type = android::CursorWindow::getFieldSlotType(fieldSlot); + if (type == CursorWindow::FIELD_TYPE_STRING) { + size_t sizeIncludingNull; + const char* value = window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull); + if (sizeIncludingNull <= 1) { + return gEmptyString; + } + const size_t MaxStackStringSize = 65536; // max size for a stack char array + if (sizeIncludingNull > MaxStackStringSize) { + auto* chars = new jchar[sizeIncludingNull - 1]; + jint size = utf8ToJavaCharArray(value, chars, sizeIncludingNull - 1); + jstring string = env->NewString(chars, size); + delete[] chars; + return string; + } else { + jchar chars[sizeIncludingNull - 1]; + jint size = utf8ToJavaCharArray(value, chars, sizeIncludingNull - 1); + return env->NewString(chars, size); + } + } else if (type == CursorWindow::FIELD_TYPE_INTEGER) { + int64_t value = android::CursorWindow::getFieldSlotValueLong(fieldSlot); + char buf[32]; + snprintf(buf, sizeof(buf), "%" PRId64, value); + return env->NewStringUTF(buf); + } else if (type == CursorWindow::FIELD_TYPE_FLOAT) { + double value = android::CursorWindow::getFieldSlotValueDouble(fieldSlot); + char buf[32]; + snprintf(buf, sizeof(buf), "%g", value); + return env->NewStringUTF(buf); + } else if (type == CursorWindow::FIELD_TYPE_NULL) { + return nullptr; + } else if (type == CursorWindow::FIELD_TYPE_BLOB) { + throw_sqlite3_exception(env, "Unable to convert BLOB to string"); + return nullptr; + } else { + throwUnknownTypeException(env, type); + return nullptr; + } + } + + static jlong nativeGetLong(JNIEnv* env, jclass clazz, jlong windowPtr, + jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column); + if (!fieldSlot) { + throwExceptionWithRowCol(env, row, column); + return 0; + } + + int32_t type = android::CursorWindow::getFieldSlotType(fieldSlot); + if (type == CursorWindow::FIELD_TYPE_INTEGER) { + return android::CursorWindow::getFieldSlotValueLong(fieldSlot); + } else if (type == CursorWindow::FIELD_TYPE_STRING) { + size_t sizeIncludingNull; + const char* value = window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull); + return sizeIncludingNull > 1 ? strtoll(value, NULL, 0) : 0L; + } else if (type == CursorWindow::FIELD_TYPE_FLOAT) { + return jlong(android::CursorWindow::getFieldSlotValueDouble(fieldSlot)); + } else if (type == CursorWindow::FIELD_TYPE_NULL) { + return 0; + } else if (type == CursorWindow::FIELD_TYPE_BLOB) { + throw_sqlite3_exception(env, "Unable to convert BLOB to long"); + return 0; + } else { + throwUnknownTypeException(env, type); + return 0; + } + } + + static jdouble nativeGetDouble(JNIEnv* env, jclass clazz, jlong windowPtr, + jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column); + if (!fieldSlot) { + throwExceptionWithRowCol(env, row, column); + return 0.0; + } + int32_t type = android::CursorWindow::getFieldSlotType(fieldSlot); + if (type == CursorWindow::FIELD_TYPE_FLOAT) { + return android::CursorWindow::getFieldSlotValueDouble(fieldSlot); + } else if (type == CursorWindow::FIELD_TYPE_STRING) { + size_t sizeIncludingNull; + const char* value = window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull); + return sizeIncludingNull > 1 ? strtod(value, NULL) : 0.0; + } else if (type == CursorWindow::FIELD_TYPE_INTEGER) { + return jdouble(android::CursorWindow::getFieldSlotValueLong(fieldSlot)); + } else if (type == CursorWindow::FIELD_TYPE_NULL) { + return 0.0; + } else if (type == CursorWindow::FIELD_TYPE_BLOB) { + throw_sqlite3_exception(env, "Unable to convert BLOB to double"); + return 0.0; + } else { + throwUnknownTypeException(env, type); + return 0.0; + } + } + + static jboolean nativePutBlob(JNIEnv* env, jclass clazz, jlong windowPtr, + jbyteArray valueObj, jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + jsize len = env->GetArrayLength(valueObj); + void* value = env->GetPrimitiveArrayCritical(valueObj, NULL); + status_t status = window->putBlob(row, column, value, len); + env->ReleasePrimitiveArrayCritical(valueObj, value, JNI_ABORT); + if (status) { + ALOGD("Failed to put blob. error=%d", status); + return false; + } + ALOGD("%d,%d is BLOB with %u bytes", row, column, len); + return true; + } + + static jboolean nativePutString(JNIEnv* env, jclass clazz, jlong windowPtr, + jstring valueObj, jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + size_t sizeIncludingNull = env->GetStringUTFLength(valueObj) + 1; + const char* valueStr = env->GetStringUTFChars(valueObj, NULL); + if (!valueStr) { + ALOGD("value can't be transferred to UTFChars"); + return false; + } + status_t status = window->putString(row, column, valueStr, sizeIncludingNull); + env->ReleaseStringUTFChars(valueObj, valueStr); + if (status) { + ALOGD("Failed to put string. error=%d", status); + return false; + } + ALOGD("%d,%d is TEXT with %zu bytes", row, column, sizeIncludingNull); + return true; + } + + static jboolean nativePutLong(JNIEnv* env, jclass clazz, jlong windowPtr, + jlong value, jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + status_t status = window->putLong(row, column, value); + if (status) { + ALOGD("Failed to put long. error=%d", status); + return false; + } + ALOGD("%d,%d is INTEGER 0x%016" PRIx64, row, column, value); + return true; + } + + static jboolean nativePutDouble(JNIEnv* env, jclass clazz, jlong windowPtr, + jdouble value, jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + status_t status = window->putDouble(row, column, value); + if (status) { + ALOGD("Failed to put double. error=%d", status); + return false; + } + ALOGD("%d,%d is FLOAT %lf", row, column, value); + return true; + } + + static jboolean nativePutNull(JNIEnv* env, jclass clazz, jlong windowPtr, + jint row, jint column) { + auto* window = reinterpret_cast(windowPtr); + status_t status = window->putNull(row, column); + if (status) { + ALOGD("Failed to put null. error=%d", status); + return false; + } + ALOGD("%d,%d is NULL", row, column); + return true; + } + + static const JNINativeMethod sMethods[] = + { + /* name, signature, funcPtr */ + { "nativeCreate", "(Ljava/lang/String;I)J", + (void*)nativeCreate }, + { "nativeDispose", "(J)V", + (void*)nativeDispose }, + { "nativeGetName", "(J)Ljava/lang/String;", + (void*)nativeGetName }, + { "nativeClear", "(J)V", + (void*)nativeClear }, + { "nativeGetNumRows", "(J)I", + (void*)nativeGetNumRows }, + { "nativeSetNumColumns", "(JI)Z", + (void*)nativeSetNumColumns }, + { "nativeAllocRow", "(J)Z", + (void*)nativeAllocRow }, + { "nativeFreeLastRow", "(J)V", + (void*)nativeFreeLastRow }, + { "nativeGetType", "(JII)I", + (void*)nativeGetType }, + { "nativeGetBlob", "(JII)[B", + (void*)nativeGetBlob }, + { "nativeGetString", "(JII)Ljava/lang/String;", + (void*)nativeGetString }, + { "nativeGetLong", "(JII)J", + (void*)nativeGetLong }, + { "nativeGetDouble", "(JII)D", + (void*)nativeGetDouble }, + { "nativePutBlob", "(J[BII)Z", + (void*)nativePutBlob }, + { "nativePutString", "(JLjava/lang/String;II)Z", + (void*)nativePutString }, + { "nativePutLong", "(JJII)Z", + (void*)nativePutLong }, + { "nativePutDouble", "(JDII)Z", + (void*)nativePutDouble }, + { "nativePutNull", "(JII)Z", + (void*)nativePutNull }, + }; + + int register_android_database_CursorWindow(JNIEnv* env) + { + jclass clazz; + FIND_CLASS(clazz, "android/database/CharArrayBuffer"); + + GET_FIELD_ID(gCharArrayBufferClassInfo.data, clazz, "data", "[C"); + GET_FIELD_ID(gCharArrayBufferClassInfo.sizeCopied, clazz, "sizeCopied", "I"); + + gEmptyString = static_cast(env->NewGlobalRef(env->NewStringUTF(""))); + return jniRegisterNativeMethods(env, + "net/zetetic/database/CursorWindow", sMethods, NELEM(sMethods)); + } + +} // namespace android diff --git a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteCommon.cpp b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteCommon.cpp index 92471c9..eac7759 100644 --- a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteCommon.cpp +++ b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteCommon.cpp @@ -24,12 +24,12 @@ namespace android { /* throw a SQLiteException with a message appropriate for the error in handle */ void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle) { - throw_sqlite3_exception(env, handle, NULL); + throw_sqlite3_exception(env, handle, nullptr); } /* throw a SQLiteException with the given message */ void throw_sqlite3_exception(JNIEnv* env, const char* message) { - throw_sqlite3_exception(env, NULL, message); + throw_sqlite3_exception(env, nullptr, message); } /* throw a SQLiteException with a message appropriate for the error in handle @@ -84,7 +84,7 @@ void throw_sqlite3_exception(JNIEnv* env, int errcode, break; case SQLITE_DONE: exceptionClass = "android/database/sqlite/SQLiteDoneException"; - sqlite3Message = NULL; // SQLite error message is irrelevant in this case + sqlite3Message = nullptr; // SQLite error message is irrelevant in this case break; case SQLITE_FULL: exceptionClass = "android/database/sqlite/SQLiteFullException"; diff --git a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp index 08d9928..dbbe2a3 100644 --- a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp +++ b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp @@ -25,14 +25,15 @@ #include "ALog-priv.h" #include -#include +#include +#include #include -#include +#include #include #include "android_database_SQLiteCommon.h" - +#include "CursorWindow.h" #include // Set to 1 to use UTF16 storage for localized indexes. @@ -94,21 +95,21 @@ struct SQLiteConnection { // Called each time a statement begins execution, when tracing is enabled. static void sqliteTraceCallback(void *data, const char *sql) { - SQLiteConnection* connection = static_cast(data); + auto* connection = static_cast(data); ALOG(LOG_VERBOSE, SQLITE_TRACE_TAG, "%s: \"%s\"\n", connection->label.c_str(), sql); } // Called each time a statement finishes execution, when profiling is enabled. static void sqliteProfileCallback(void *data, const char *sql, sqlite3_uint64 tm) { - SQLiteConnection* connection = static_cast(data); + auto* connection = static_cast(data); ALOG(LOG_VERBOSE, SQLITE_PROFILE_TAG, "%s: \"%s\" took %0.3f ms\n", connection->label.c_str(), sql, tm * 0.000001f); } // Called after each SQLite VM instruction when cancelation is enabled. static int sqliteProgressHandlerCallback(void* data) { - SQLiteConnection* connection = static_cast(data); + auto* connection = static_cast(data); return connection->canceled; } @@ -227,7 +228,7 @@ static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFla } // Create wrapper object. - SQLiteConnection* connection = new SQLiteConnection(db, openFlags, path, label); + auto* connection = new SQLiteConnection(db, openFlags, path, label); // Enable tracing and profiling if requested. if (enableTrace) { @@ -242,7 +243,7 @@ static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFla } static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); + auto* connection = reinterpret_cast(connectionPtr); if (connection) { ALOGV("Closing connection %p", connection->db); @@ -268,13 +269,13 @@ static void sqliteCustomFunctionCallback(sqlite3_context *context, // Get the callback function object. // Create a new local reference to it in case the callback tries to do something // dumb like unregister the function (thereby destroying the global ref) while it is running. - jobject functionObjGlobal = reinterpret_cast(sqlite3_user_data(context)); + auto functionObjGlobal = reinterpret_cast(sqlite3_user_data(context)); jobject functionObj = env->NewLocalRef(functionObjGlobal); jobjectArray argsArray = env->NewObjectArray(argc, gStringClassInfo.clazz, NULL); if (argsArray) { for (int i = 0; i < argc; i++) { - const jchar* arg = static_cast(sqlite3_value_text16(argv[i])); + const auto* arg = static_cast(sqlite3_value_text16(argv[i])); if (!arg) { ALOGW("NULL argument in custom_function_callback. This should not happen."); } else { @@ -307,7 +308,7 @@ static void sqliteCustomFunctionCallback(sqlite3_context *context, // Called when a custom function is destroyed. static void sqliteCustomFunctionDestructor(void* data) { - jobject functionObjGlobal = reinterpret_cast(data); + auto functionObjGlobal = reinterpret_cast(data); JNIEnv* env = 0; gpJavaVM->GetEnv((void**)&env, JNI_VERSION_1_4); env->DeleteGlobalRef(functionObjGlobal); @@ -315,9 +316,9 @@ static void sqliteCustomFunctionDestructor(void* data) { static void nativeRegisterCustomFunction(JNIEnv* env, jclass clazz, jlong connectionPtr, jobject functionObj) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); + auto* connection = reinterpret_cast(connectionPtr); - jstring nameStr = jstring(env->GetObjectField( + auto nameStr = jstring(env->GetObjectField( functionObj, gSQLiteCustomFunctionClassInfo.name)); jint numArgs = env->GetIntField(functionObj, gSQLiteCustomFunctionClassInfo.numArgs); @@ -344,7 +345,7 @@ static void nativeRegisterLocalizedCollators(JNIEnv* env, jclass clazz, jlong co static jlong nativePrepareStatement(JNIEnv* env, jclass clazz, jlong connectionPtr, jstring sqlString) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); + auto* connection = reinterpret_cast(connectionPtr); jsize sqlLength = env->GetStringLength(sqlString); const jchar* sql = env->GetStringCritical(sqlString, NULL); @@ -375,8 +376,8 @@ static jlong nativePrepareStatement(JNIEnv* env, jclass clazz, jlong connectionP static void nativeFinalizeStatement(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); // We ignore the result of sqlite3_finalize because it is really telling us about // whether any errors occurred while executing the statement. The statement itself @@ -387,30 +388,26 @@ static void nativeFinalizeStatement(JNIEnv* env, jclass clazz, jlong connectionP static jint nativeGetParameterCount(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* statement = reinterpret_cast(statementPtr); return sqlite3_bind_parameter_count(statement); } static jboolean nativeIsReadOnly(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* statement = reinterpret_cast(statementPtr); return sqlite3_stmt_readonly(statement) != 0; } static jint nativeGetColumnCount(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* statement = reinterpret_cast(statementPtr); return sqlite3_column_count(statement); } static jstring nativeGetColumnName(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index) { - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - - const jchar* name = static_cast(sqlite3_column_name16(statement, index)); + auto* statement = reinterpret_cast(statementPtr); + const auto* name = static_cast(sqlite3_column_name16(statement, index)); if (name) { size_t length = 0; while (name[length]) { @@ -418,14 +415,13 @@ static jstring nativeGetColumnName(JNIEnv* env, jclass clazz, jlong connectionPt } return env->NewString(name, length); } - return NULL; + return nullptr; } static void nativeBindNull(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = sqlite3_bind_null(statement, index); if (err != SQLITE_OK) { throw_sqlite3_exception(env, connection->db, NULL); @@ -434,9 +430,8 @@ static void nativeBindNull(JNIEnv* env, jclass clazz, jlong connectionPtr, static void nativeBindLong(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index, jlong value) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = sqlite3_bind_int64(statement, index, value); if (err != SQLITE_OK) { throw_sqlite3_exception(env, connection->db, NULL); @@ -445,9 +440,8 @@ static void nativeBindLong(JNIEnv* env, jclass clazz, jlong connectionPtr, static void nativeBindDouble(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index, jdouble value) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = sqlite3_bind_double(statement, index, value); if (err != SQLITE_OK) { throw_sqlite3_exception(env, connection->db, NULL); @@ -456,9 +450,8 @@ static void nativeBindDouble(JNIEnv* env, jclass clazz, jlong connectionPtr, static void nativeBindString(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index, jstring valueString) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); jsize valueLength = env->GetStringLength(valueString); const jchar* value = env->GetStringCritical(valueString, NULL); int err = sqlite3_bind_text16(statement, index, value, valueLength * sizeof(jchar), @@ -471,11 +464,10 @@ static void nativeBindString(JNIEnv* env, jclass clazz, jlong connectionPtr, static void nativeBindBlob(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index, jbyteArray valueArray) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); jsize valueLength = env->GetArrayLength(valueArray); - jbyte* value = static_cast(env->GetPrimitiveArrayCritical(valueArray, NULL)); + auto* value = static_cast(env->GetPrimitiveArrayCritical(valueArray, NULL)); int err = sqlite3_bind_blob(statement, index, value, valueLength, SQLITE_TRANSIENT); env->ReleasePrimitiveArrayCritical(valueArray, value, JNI_ABORT); if (err != SQLITE_OK) { @@ -485,9 +477,8 @@ static void nativeBindBlob(JNIEnv* env, jclass clazz, jlong connectionPtr, static void nativeResetStatementAndClearBindings(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = sqlite3_reset(statement); if (err == SQLITE_OK) { err = sqlite3_clear_bindings(statement); @@ -507,9 +498,8 @@ static int executeNonQueryRaw(JNIEnv* env, SQLiteConnection* connection, sqlite3 static void nativeExecuteRaw(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); executeNonQueryRaw(env, connection, statement); } @@ -526,26 +516,23 @@ static int executeNonQuery(JNIEnv* env, SQLiteConnection* connection, sqlite3_st static void nativeExecute(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); executeNonQuery(env, connection, statement); } static jint nativeExecuteForChangedRowCount(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = executeNonQuery(env, connection, statement); return err == SQLITE_DONE ? sqlite3_changes(connection->db) : -1; } static jlong nativeExecuteForLastInsertedRowId(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = executeNonQuery(env, connection, statement); return err == SQLITE_DONE && sqlite3_changes(connection->db) > 0 ? sqlite3_last_insert_rowid(connection->db) : -1; @@ -561,9 +548,8 @@ static int executeOneRowQuery(JNIEnv* env, SQLiteConnection* connection, sqlite3 static jlong nativeExecuteForLong(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = executeOneRowQuery(env, connection, statement); if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) { return sqlite3_column_int64(statement, 0); @@ -573,9 +559,8 @@ static jlong nativeExecuteForLong(JNIEnv* env, jclass clazz, static jstring nativeExecuteForString(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = executeOneRowQuery(env, connection, statement); if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) { const jchar* text = static_cast(sqlite3_column_text16(statement, 0)); @@ -584,7 +569,7 @@ static jstring nativeExecuteForString(JNIEnv* env, jclass clazz, return env->NewString(text, length); } } - return NULL; + return nullptr; } static int createAshmemRegionWithData(JNIEnv* env, const void* data, size_t length) { @@ -594,9 +579,8 @@ static int createAshmemRegionWithData(JNIEnv* env, const void* data, size_t leng static jint nativeExecuteForBlobFileDescriptor(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - sqlite3_stmt* statement = reinterpret_cast(statementPtr); - + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); int err = executeOneRowQuery(env, connection, statement); if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) { const void* blob = sqlite3_column_blob(statement, 0); @@ -610,245 +594,203 @@ static jint nativeExecuteForBlobFileDescriptor(JNIEnv* env, jclass clazz, return -1; } -/* -** Note: The following symbols must be in the same order as the corresponding -** elements in the aMethod[] array in function nativeExecuteForCursorWindow(). -*/ -enum CWMethodNames { - CW_CLEAR = 0, - CW_SETNUMCOLUMNS = 1, - CW_ALLOCROW = 2, - CW_FREELASTROW = 3, - CW_PUTNULL = 4, - CW_PUTLONG = 5, - CW_PUTDOUBLE = 6, - CW_PUTSTRING = 7, - CW_PUTBLOB = 8 -}; - -/* -** An instance of this structure represents a single CursorWindow java method. -*/ -struct CWMethod { - jmethodID id; /* Method id */ - const char *zName; /* Method name */ - const char *zSig; /* Method JNI signature */ +enum CopyRowResult { + CPR_OK, + CPR_FULL, + CPR_ERROR, }; -/* -** Append the contents of the row that SQL statement pStmt currently points to -** to the CursorWindow object passed as the second argument. The CursorWindow -** currently contains iRow rows. Return true on success or false if an error -** occurs. -*/ -static jboolean copyRowToWindow( - JNIEnv *pEnv, - jobject win, - int iRow, - sqlite3_stmt *pStmt, - CWMethod *aMethod -){ - int nCol = sqlite3_column_count(pStmt); - int i; - jboolean bOk; - - bOk = pEnv->CallBooleanMethod(win, aMethod[CW_ALLOCROW].id); - for(i=0; bOk && iCallBooleanMethod(win, aMethod[CW_PUTNULL].id, iRow, i); - break; - } - - case SQLITE_INTEGER: { - jlong val = sqlite3_column_int64(pStmt, i); - bOk = pEnv->CallBooleanMethod(win, aMethod[CW_PUTLONG].id, val, iRow, i); - break; - } - - case SQLITE_FLOAT: { - jdouble val = sqlite3_column_double(pStmt, i); - bOk = pEnv->CallBooleanMethod(win, aMethod[CW_PUTDOUBLE].id, val, iRow, i); - break; - } - - case SQLITE_TEXT: { - jchar *pStr = (jchar*)sqlite3_column_text16(pStmt, i); - int nStr = sqlite3_column_bytes16(pStmt, i) / sizeof(jchar); - jstring val = pEnv->NewString(pStr, nStr); - bOk = pEnv->CallBooleanMethod(win, aMethod[CW_PUTSTRING].id, val, iRow, i); - pEnv->DeleteLocalRef(val); - break; - } - - default: { - assert( sqlite3_column_type(pStmt, i)==SQLITE_BLOB ); - const jbyte *p = (const jbyte*)sqlite3_column_blob(pStmt, i); - int n = sqlite3_column_bytes(pStmt, i); - jbyteArray val = pEnv->NewByteArray(n); - pEnv->SetByteArrayRegion(val, 0, n, p); - bOk = pEnv->CallBooleanMethod(win, aMethod[CW_PUTBLOB].id, val, iRow, i); - pEnv->DeleteLocalRef(val); - break; - } - } - - if( bOk==0 ){ - pEnv->CallVoidMethod(win, aMethod[CW_FREELASTROW].id); +static CopyRowResult copyRow(JNIEnv* env, CursorWindow* window, + sqlite3_stmt* statement, int numColumns, int startPos, int addedRows) { + // Allocate a new field directory for the row. + status_t status = window->allocRow(); + if (status) { + ALOGD("Failed allocating fieldDir at startPos %d row %d, error=%d", + startPos, addedRows, status); + return CPR_FULL; + } + + // Pack the row into the window. + CopyRowResult result = CPR_OK; + for (int i = 0; i < numColumns; i++) { + int type = sqlite3_column_type(statement, i); + if (type == SQLITE_TEXT) { + // TEXT data + const char* text = reinterpret_cast( + sqlite3_column_text(statement, i)); + // SQLite does not include the NULL terminator in size, but does + // ensure all strings are NULL terminated, so increase size by + // one to make sure we store the terminator. + size_t sizeIncludingNull = sqlite3_column_bytes(statement, i) + 1; + status = window->putString(addedRows, i, text, sizeIncludingNull); + if (status) { + ALOGD("Failed allocating %zu bytes for text at %d,%d, error=%d", + sizeIncludingNull, startPos + addedRows, i, status); + result = CPR_FULL; + break; + } + ALOGD("%d,%d is TEXT with %zu bytes", + startPos + addedRows, i, sizeIncludingNull); + } else if (type == SQLITE_INTEGER) { + // INTEGER data + int64_t value = sqlite3_column_int64(statement, i); + status = window->putLong(addedRows, i, value); + if (status) { + ALOGD("Failed allocating space for a long in column %d, error=%d", + i, status); + result = CPR_FULL; + break; + } + ALOGD("%d,%d is INTEGER 0x%016" PRIx64, startPos + addedRows, i, value); + } else if (type == SQLITE_FLOAT) { + // FLOAT data + double value = sqlite3_column_double(statement, i); + status = window->putDouble(addedRows, i, value); + if (status) { + ALOGD("Failed allocating space for a double in column %d, error=%d", + i, status); + result = CPR_FULL; + break; + } + ALOGD("%d,%d is FLOAT %lf", startPos + addedRows, i, value); + } else if (type == SQLITE_BLOB) { + // BLOB data + const void* blob = sqlite3_column_blob(statement, i); + size_t size = sqlite3_column_bytes(statement, i); + status = window->putBlob(addedRows, i, blob, size); + if (status) { + ALOGD("Failed allocating %zu bytes for blob at %d,%d, error=%d", + size, startPos + addedRows, i, status); + result = CPR_FULL; + break; + } + ALOGD("%d,%d is Blob with %zu bytes", + startPos + addedRows, i, size); + } else if (type == SQLITE_NULL) { + // NULL field + status = window->putNull(addedRows, i); + if (status) { + ALOGD("Failed allocating space for a null in column %d, error=%d", + i, status); + result = CPR_FULL; + break; + } + ALOGD("%d,%d is NULL", startPos + addedRows, i); + } else { + // Unknown data + ALOGE("Unknown column type when filling database window"); + throw_sqlite3_exception(env, "Unknown column type when filling window"); + result = CPR_ERROR; + break; + } } - } - - return bOk; -} - -static jboolean setWindowNumColumns( - JNIEnv *pEnv, - jobject win, - sqlite3_stmt *pStmt, - CWMethod *aMethod -){ - int nCol; - pEnv->CallVoidMethod(win, aMethod[CW_CLEAR].id); - nCol = sqlite3_column_count(pStmt); - return pEnv->CallBooleanMethod(win, aMethod[CW_SETNUMCOLUMNS].id, (jint)nCol); + // Free the last row if if was not successfully copied. + if (result != CPR_OK) { + window->freeLastRow(); + } + return result; } -/* -** This method has been rewritten for org.sqlite.database.*. The original -** android implementation used the C++ interface to populate a CursorWindow -** object. Since the NDK does not export this interface, we invoke the Java -** interface using standard JNI methods to do the same thing. -** -** This function executes the SQLite statement object passed as the 4th -** argument and copies one or more returned rows into the CursorWindow -** object passed as the 5th argument. The set of rows copied into the -** CursorWindow is always contiguous. -** -** The only row that *must* be copied into the CursorWindow is row -** iRowRequired. Ideally, all rows from iRowStart through to the end -** of the query are copied into the CursorWindow. If this is not possible -** (CursorWindow objects have a finite capacity), some compromise position -** is found (see comments embedded in the code below for details). -** -** The return value is a 64-bit integer calculated as follows: -** -** (iStart << 32) | nRow -** -** where iStart is the index of the first row copied into the CursorWindow. -** If the countAllRows argument is true, nRow is the total number of rows -** returned by the query. Otherwise, nRow is one greater than the index of -** the last row copied into the CursorWindow. -*/ static jlong nativeExecuteForCursorWindow( - JNIEnv *pEnv, - jclass clazz, - jlong connectionPtr, /* Pointer to SQLiteConnection C++ object */ - jlong statementPtr, /* Pointer to sqlite3_stmt object */ - jobject win, /* The CursorWindow object to populate */ - jint startPos, /* First row to add (advisory) */ - jint iRowRequired, /* Required row */ - jboolean countAllRows -) { - SQLiteConnection *pConnection = reinterpret_cast(connectionPtr); - sqlite3_stmt *pStmt = reinterpret_cast(statementPtr); - - CWMethod aMethod[] = { - {0, "clear", "()V"}, - {0, "setNumColumns", "(I)Z"}, - {0, "allocRow", "()Z"}, - {0, "freeLastRow", "()V"}, - {0, "putNull", "(II)Z"}, - {0, "putLong", "(JII)Z"}, - {0, "putDouble", "(DII)Z"}, - {0, "putString", "(Ljava/lang/String;II)Z"}, - {0, "putBlob", "([BII)Z"}, - }; - jclass cls; /* Class android.database.CursorWindow */ - int i; /* Iterator variable */ - int nCol; /* Number of columns returned by pStmt */ - int nRow; - jboolean bOk; - int iStart; /* First row copied to CursorWindow */ - int rc_step; - /* Locate all required CursorWindow methods. */ - cls = pEnv->FindClass("android/database/CursorWindow"); - for(i=0; i<(sizeof(aMethod)/sizeof(struct CWMethod)); i++){ - aMethod[i].id = pEnv->GetMethodID(cls, aMethod[i].zName, aMethod[i].zSig); - if( aMethod[i].id==NULL ){ - jniThrowExceptionFmt(pEnv, "java/lang/Exception", - "Failed to find method CursorWindow.%s()", aMethod[i].zName - ); - return 0; + JNIEnv* env, + jclass clazz, + jlong connectionPtr, + jlong statementPtr, + jlong windowPtr, + jint startPos, + jint requiredPos, + jboolean countAllRows){ + auto* connection = reinterpret_cast(connectionPtr); + auto* statement = reinterpret_cast(statementPtr); + auto* window = reinterpret_cast(windowPtr); + status_t status = window->clear(); + if (status) { + throw_sqlite3_exception(env, connection->db, "Failed to clear the cursor window"); + return 0; } - } + int numColumns = sqlite3_column_count(statement); + status = window->setNumColumns(numColumns); + if (status) { + throw_sqlite3_exception(env, connection->db, "Failed to set the cursor window column count"); + return 0; + } + int retryCount = 0; + int totalRows = 0; + int addedRows = 0; + bool windowFull = false; + bool gotException = false; + while (!gotException && (!windowFull || countAllRows)) { + int err = sqlite3_step(statement); + if (err == SQLITE_ROW) { + ALOGD("Stepped statement %p to row %d", statement, totalRows); + retryCount = 0; + totalRows += 1; + + // Skip the row if the window is full or we haven't reached the start position yet. + if (startPos >= totalRows || windowFull) { + continue; + } + CopyRowResult cpr = copyRow(env, window, statement, numColumns, startPos, addedRows); + if (cpr == CPR_FULL && addedRows && startPos + addedRows <= requiredPos) { + // We filled the window before we got to the one row that we really wanted. + // Clear the window and start filling it again from here. + // TODO: Would be nicer if we could progressively replace earlier rows. + window->clear(); + window->setNumColumns(numColumns); + startPos += addedRows; + addedRows = 0; + cpr = copyRow(env, window, statement, numColumns, startPos, addedRows); + if(cpr == CPR_FULL){ + throw_sqlite3_exception(env, "Row too big to fit in CursorWindow"); + } + } - /* Set the number of columns in the window */ - bOk = setWindowNumColumns(pEnv, win, pStmt, aMethod); - if( bOk==0 ) return 0; - - nRow = 0; - iStart = startPos; - while( (rc_step = sqlite3_step(pStmt))==SQLITE_ROW ){ - /* Only copy in rows that occur at or after row index iStart. */ - if( nRow>=iStart && bOk ){ - bOk = copyRowToWindow(pEnv, win, (nRow - iStart), pStmt, aMethod); - if( bOk==0 ){ - /* The CursorWindow object ran out of memory. If row iRowRequired was - ** not successfully added before this happened, clear the CursorWindow - ** and try to add the current row again. */ - if( nRow<=iRowRequired ){ - bOk = setWindowNumColumns(pEnv, win, pStmt, aMethod); - if( bOk==0 ){ - sqlite3_reset(pStmt); - return 0; - } - iStart = nRow; - bOk = copyRowToWindow(pEnv, win, (nRow - iStart), pStmt, aMethod); + if (cpr == CPR_OK) { + addedRows += 1; + } else if (cpr == CPR_FULL) { + windowFull = true; + } else { + gotException = true; + } + } else if (err == SQLITE_DONE) { + // All rows processed, bail + ALOGD("Processed all rows"); + break; + } else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) { + // The table is locked, retry + ALOGD("Database locked, retrying"); + if (retryCount > 50) { + ALOGE("Bailing on database busy retry"); + throw_sqlite3_exception(env, connection->db, "retrycount exceeded"); + gotException = true; + } else { + // Sleep to give the thread holding the lock a chance to finish + usleep(1000); + retryCount++; + } + } else { + throw_sqlite3_exception(env, connection->db); + gotException = true; } - - /* If the CursorWindow is still full and the countAllRows flag is not - ** set, break out of the loop here. If countAllRows is set, continue - ** so as to set variable nRow correctly. */ - if( bOk==0 && countAllRows==0 ) break; - } } - nRow++; - } - - /* Finalize the statement. If this indicates an error occurred, throw an - ** SQLiteException exception. */ - int rc = sqlite3_reset(pStmt); - if( rc!=SQLITE_OK ){ - throw_sqlite3_exception(pEnv, sqlite3_db_handle(pStmt)); - return 0; - } - - if( bOk==0 && countAllRows==0 && nRow <= iRowRequired ) { - char *msg = sqlite3_mprintf("Row too big to fit into CursorWindow requiredPos=%d, totalRows=%d", - iRowRequired, nRow); - throw_sqlite3_exception(pEnv, pConnection->db, msg); - sqlite3_free(msg); - return 0; - } + ALOGD("Resetting statement %p after fetching %d rows and adding %d rows" + "to the window in %zu bytes", + statement, totalRows, addedRows, window->size() - window->freeSpace()); + sqlite3_reset(statement); - if ( rc_step != SQLITE_DONE && rc_step != SQLITE_ROW ) { - char *msg = sqlite3_mprintf("Unexpected result code=%d while stepping through result", - rc_step); - throw_sqlite3_exception(pEnv, pConnection->db, msg); - sqlite3_free(msg); - return 0; - } - - jlong lRet = jlong(iStart) << 32 | jlong(nRow); - return lRet; + // Report the total number of rows on request. + if (startPos > totalRows) { + ALOGE("startPos %d > actual rows %d", startPos, totalRows); + } + jlong result = jlong(startPos) << 32 | jlong(totalRows); + return result; } static jint nativeGetDbLookaside(JNIEnv* env, jobject clazz, jlong connectionPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); - + auto* connection = reinterpret_cast(connectionPtr); int cur = -1; int unused; sqlite3_db_status(connection->db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur, &unused, 0); @@ -856,15 +798,14 @@ static jint nativeGetDbLookaside(JNIEnv* env, jobject clazz, jlong connectionPtr } static void nativeCancel(JNIEnv* env, jobject clazz, jlong connectionPtr) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); + auto* connection = reinterpret_cast(connectionPtr); connection->canceled = true; } static void nativeResetCancel(JNIEnv* env, jobject clazz, jlong connectionPtr, jboolean cancelable) { - SQLiteConnection* connection = reinterpret_cast(connectionPtr); + auto* connection = reinterpret_cast(connectionPtr); connection->canceled = false; - if (cancelable) { sqlite3_progress_handler(connection->db, 4, sqliteProgressHandlerCallback, connection); @@ -935,7 +876,7 @@ static JNINativeMethod sMethods[] = (void*)nativeExecuteForChangedRowCount }, { "nativeExecuteForLastInsertedRowId", "(JJ)J", (void*)nativeExecuteForLastInsertedRowId }, - { "nativeExecuteForCursorWindow", "(JJLandroid/database/CursorWindow;IIZ)J", + { "nativeExecuteForCursorWindow", "(JJJIIZ)J", (void*)nativeExecuteForCursorWindow }, { "nativeGetDbLookaside", "(J)I", (void*)nativeGetDbLookaside }, @@ -947,18 +888,6 @@ static JNINativeMethod sMethods[] = { "nativeHasCodec", "()Z", (void*)nativeHasCodec }, }; -#define FIND_CLASS(var, className) \ - var = env->FindClass(className); \ - LOG_FATAL_IF(! var, "Unable to find class " className); - -#define GET_METHOD_ID(var, clazz, methodName, fieldDescriptor) \ - var = env->GetMethodID(clazz, methodName, fieldDescriptor); \ - LOG_FATAL_IF(! var, "Unable to find method" methodName); - -#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \ - var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \ - LOG_FATAL_IF(! var, "Unable to find field " fieldName); - int register_android_database_SQLiteConnection(JNIEnv *env) { jclass clazz; @@ -982,6 +911,7 @@ int register_android_database_SQLiteConnection(JNIEnv *env) extern int register_android_database_SQLiteGlobal(JNIEnv *env); extern int register_android_database_SQLiteDebug(JNIEnv *env); +extern int register_android_database_CursorWindow(JNIEnv *env); } // namespace android @@ -994,6 +924,7 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { android::register_android_database_SQLiteConnection(env); android::register_android_database_SQLiteDebug(env); android::register_android_database_SQLiteGlobal(env); + android::register_android_database_CursorWindow(env); return JNI_VERSION_1_4; } diff --git a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteDebug.cpp b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteDebug.cpp index d716ee4..e3d3f56 100644 --- a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteDebug.cpp +++ b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteDebug.cpp @@ -24,9 +24,9 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include @@ -76,14 +76,14 @@ static JNINativeMethod gMethods[] = int register_android_database_SQLiteDebug(JNIEnv *env) { jclass clazz; - FIND_CLASS(clazz, "net/zetetic/database/sqlcipher/SQLiteDebug$PagerStats"); + FIND_CLASS(clazz, "net/zetetic/database/sqlcipher/SQLiteDebug$PagerStats") GET_FIELD_ID(gSQLiteDebugPagerStatsClassInfo.memoryUsed, clazz, - "memoryUsed", "I"); + "memoryUsed", "I") GET_FIELD_ID(gSQLiteDebugPagerStatsClassInfo.largestMemAlloc, clazz, - "largestMemAlloc", "I"); + "largestMemAlloc", "I") GET_FIELD_ID(gSQLiteDebugPagerStatsClassInfo.pageCacheOverflow, clazz, - "pageCacheOverflow", "I"); + "pageCacheOverflow", "I") return jniRegisterNativeMethods(env, "net/zetetic/database/sqlcipher/SQLiteDebug", gMethods, NELEM(gMethods)); diff --git a/sqlcipher/src/main/jni/sqlcipher/nativehelper/JNIHelp.h b/sqlcipher/src/main/jni/sqlcipher/nativehelper/JNIHelp.h index 758b7c7..34f04df 100644 --- a/sqlcipher/src/main/jni/sqlcipher/nativehelper/JNIHelp.h +++ b/sqlcipher/src/main/jni/sqlcipher/nativehelper/JNIHelp.h @@ -188,4 +188,16 @@ inline void jniLogException(JNIEnv* env, int priority, const char* tag, jthrowab _rc; }) #endif +#define FIND_CLASS(var, className) \ + var = env->FindClass(className); \ + LOG_FATAL_IF(! var, "Unable to find class " className); + +#define GET_METHOD_ID(var, clazz, methodName, fieldDescriptor) \ + var = env->GetMethodID(clazz, methodName, fieldDescriptor); \ + LOG_FATAL_IF(! var, "Unable to find method" methodName); + +#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \ + var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \ + LOG_FATAL_IF(! var, "Unable to find field " fieldName); + #endif /* NATIVEHELPER_JNIHELP_H_ */ From 136b9760899b00a9ccb2090d1f3a01b482f6713c Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Wed, 15 Jan 2025 15:57:44 -0600 Subject: [PATCH 16/25] Bump Java version to 17 --- sqlcipher/build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sqlcipher/build.gradle b/sqlcipher/build.gradle index 3fff3d1..b1c7a29 100644 --- a/sqlcipher/build.gradle +++ b/sqlcipher/build.gradle @@ -50,9 +50,10 @@ android { useLibrary 'android.test.base' // for android.test.AndroidTestCase useLibrary 'android.test.runner' // for android.test.MoreAsserts - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } publishing { From c153d201688b7adbc7e724388d2c0b4974fa7499 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Wed, 15 Jan 2025 15:58:16 -0600 Subject: [PATCH 17/25] Remove package name from manifest due to compiler warning --- sqlcipher/src/main/AndroidManifest.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sqlcipher/src/main/AndroidManifest.xml b/sqlcipher/src/main/AndroidManifest.xml index fc4c1a9..a2f47b6 100644 --- a/sqlcipher/src/main/AndroidManifest.xml +++ b/sqlcipher/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - + From 1620b95894b0fe25117cf031cf146f1f969edcb8 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Fri, 17 Jan 2025 08:40:42 -0600 Subject: [PATCH 18/25] Include CursorWindow/Errors header files --- .../src/main/jni/sqlcipher/CursorWindow.h | 178 ++++++++++++++++++ sqlcipher/src/main/jni/sqlcipher/Errors.h | 88 +++++++++ 2 files changed, 266 insertions(+) create mode 100644 sqlcipher/src/main/jni/sqlcipher/CursorWindow.h create mode 100644 sqlcipher/src/main/jni/sqlcipher/Errors.h diff --git a/sqlcipher/src/main/jni/sqlcipher/CursorWindow.h b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.h new file mode 100644 index 0000000..769ed79 --- /dev/null +++ b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.h @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// modified from original source see README at the top level of this project + +#ifndef _ANDROID__DATABASE_WINDOW_H +#define _ANDROID__DATABASE_WINDOW_H + +#include "ALog-priv.h" +#include +#include + +#include "Errors.h" + +namespace android { + +/** + * This class stores a set of rows from a database in a buffer. The beginning of the + * window has first chunk of RowSlots, which are offsets to the row directory, followed by + * an offset to the next chunk in a linked-list of additional chunk of RowSlots in case + * the pre-allocated chunk isn't big enough to refer to all rows. Each row directory has a + * FieldSlot per column, which has the size, offset, and type of the data for that field. + * Note that the data types come from sqlite3.h. + * + * Strings are stored in UTF-8. + */ + class CursorWindow { + CursorWindow(const char* name, void* data, size_t size, bool readOnly); + + public: + /* Field types. */ + enum { + FIELD_TYPE_NULL = 0, + FIELD_TYPE_INTEGER = 1, + FIELD_TYPE_FLOAT = 2, + FIELD_TYPE_STRING = 3, + FIELD_TYPE_BLOB = 4, + }; + + /* Opaque type that describes a field slot. */ + struct FieldSlot { + private: + int32_t type; + union { + double d; + int64_t l; + struct { + uint32_t offset; + uint32_t size; + } buffer; + } data; + + friend class CursorWindow; + } __attribute((packed)); + + ~CursorWindow(); + + static status_t create(const char* name, size_t size, CursorWindow** outCursorWindow); + + inline const char* name() { return mName; } + inline size_t size() const { return mSize; } + inline size_t freeSpace() { return mSize - mHeader->freeOffset; } + inline uint32_t getNumRows() { return mHeader->numRows; } + inline uint32_t getNumColumns() { return mHeader->numColumns; } + + status_t clear(); + status_t setNumColumns(uint32_t numColumns); + + /** + * Allocate a row slot and its directory. + * The row is initialized will null entries for each field. + */ + status_t allocRow(); + status_t freeLastRow(); + status_t maybeInflate(); + + status_t putBlob(uint32_t row, uint32_t column, const void* value, size_t size); + status_t putString(uint32_t row, uint32_t column, const char* value, size_t sizeIncludingNull); + status_t putLong(uint32_t row, uint32_t column, int64_t value); + status_t putDouble(uint32_t row, uint32_t column, double value); + status_t putNull(uint32_t row, uint32_t column); + + /** + * Gets the field slot at the specified row and column. + * Returns null if the requested row or column is not in the window. + */ + FieldSlot* getFieldSlot(uint32_t row, uint32_t column); + + static inline int32_t getFieldSlotType(FieldSlot* fieldSlot) { + return fieldSlot->type; + } + + static inline int64_t getFieldSlotValueLong(FieldSlot* fieldSlot) { + return fieldSlot->data.l; + } + + static inline double getFieldSlotValueDouble(FieldSlot* fieldSlot) { + return fieldSlot->data.d; + } + + inline const char* getFieldSlotValueString(FieldSlot* fieldSlot, + size_t* outSizeIncludingNull) { + *outSizeIncludingNull = fieldSlot->data.buffer.size; + return static_cast(offsetToPtr(fieldSlot->data.buffer.offset)); + } + + inline const void* getFieldSlotValueBlob(FieldSlot* fieldSlot, size_t* outSize) { + *outSize = fieldSlot->data.buffer.size; + return offsetToPtr(fieldSlot->data.buffer.offset); + } + + private: + static const size_t ROW_SLOT_CHUNK_NUM_ROWS = 100; + static const size_t CURSOR_SIZE_EXTRA = 512; + + struct Header { + // Offset of the lowest unused byte in the window. + uint32_t freeOffset; + + // Offset of the first row slot chunk. + uint32_t firstChunkOffset; + + uint32_t numRows; + uint32_t numColumns; + }; + + struct RowSlot { + uint32_t offset; + }; + + struct RowSlotChunk { + RowSlot slots[ROW_SLOT_CHUNK_NUM_ROWS]; + uint32_t nextChunkOffset; + }; + + char* mName; + void* mData; + size_t mSize; + bool mReadOnly; + Header* mHeader; + + inline void* offsetToPtr(uint32_t offset) { + return static_cast(mData) + offset; + } + + inline uint32_t offsetFromPtr(void* ptr) { + return static_cast(ptr) - static_cast(mData); + } + + /** + * Allocate a portion of the window. Returns the offset + * of the allocation, or 0 if there isn't enough space. + * If aligned is true, the allocation gets 4 byte alignment. + */ + uint32_t alloc(size_t size, bool aligned = false); + + RowSlot* getRowSlot(uint32_t row); + RowSlot* allocRowSlot(); + + status_t putBlobOrString(uint32_t row, uint32_t column, + const void* value, size_t size, int32_t type); + }; + +} // namespace android + +#endif diff --git a/sqlcipher/src/main/jni/sqlcipher/Errors.h b/sqlcipher/src/main/jni/sqlcipher/Errors.h new file mode 100644 index 0000000..64b0564 --- /dev/null +++ b/sqlcipher/src/main/jni/sqlcipher/Errors.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_ERRORS_H +#define ANDROID_ERRORS_H + +#include +#include + +namespace android { + +// use this type to return error codes +#ifdef HAVE_MS_C_RUNTIME + typedef int status_t; +#else + typedef int32_t status_t; +#endif + +/* the MS C runtime lacks a few error codes */ + +/* + * Error codes. + * All error codes are negative values. + */ + +// Win32 #defines NO_ERROR as well. It has the same value, so there's no +// real conflict, though it's a bit awkward. +#ifdef _WIN32 +# undef NO_ERROR +#endif + + enum { + OK = 0, // Everything's swell. + NO_ERROR = 0, // No errors. + + UNKNOWN_ERROR = 0x80000000, + + NO_MEMORY = -ENOMEM, + INVALID_OPERATION = -ENOSYS, + BAD_VALUE = -EINVAL, + BAD_TYPE = 0x80000001, + NAME_NOT_FOUND = -ENOENT, + PERMISSION_DENIED = -EPERM, + NO_INIT = -ENODEV, + ALREADY_EXISTS = -EEXIST, + DEAD_OBJECT = -EPIPE, + FAILED_TRANSACTION = 0x80000002, + JPARKS_BROKE_IT = -EPIPE, +#if !defined(HAVE_MS_C_RUNTIME) + BAD_INDEX = -EOVERFLOW, + NOT_ENOUGH_DATA = -ENODATA, + WOULD_BLOCK = -EWOULDBLOCK, + TIMED_OUT = -ETIMEDOUT, + UNKNOWN_TRANSACTION = -EBADMSG, +#else + BAD_INDEX = -E2BIG, + NOT_ENOUGH_DATA = 0x80000003, + WOULD_BLOCK = 0x80000004, + TIMED_OUT = 0x80000005, + UNKNOWN_TRANSACTION = 0x80000006, +#endif + FDS_NOT_ALLOWED = 0x80000007, + }; + +// Restore define; enumeration is in "android" namespace, so the value defined +// there won't work for Win32 code in a different namespace. +#ifdef _WIN32 +# define NO_ERROR 0L +#endif + +}; // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_ERRORS_H From 52ced90cf995867aff3a92905f7d5e881400bcc6 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Fri, 17 Jan 2025 09:05:18 -0600 Subject: [PATCH 19/25] Ignore amalgamation files from the sqlcipher directory --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6e37075..d0d2980 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ sqlciphertest/build *.iml local.properties sqlcipher/.cxx -sqlcipher/src/main/jni/sqlcipher/*.c -sqlcipher/src/main/jni/sqlcipher/*.h +sqlcipher/src/main/jni/sqlcipher/sqlite3.c +sqlcipher/src/main/jni/sqlcipher/sqlite3.h sqlcipher/src/main/jni/sqlcipher/android-libs build \ No newline at end of file From 71e48d013da6e574806196cdaa55d5f5f7759bd6 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Tue, 21 Jan 2025 13:04:14 -0600 Subject: [PATCH 20/25] Update unit tests from deprecated AndroidTestCase to AndroidJUnit4 This also suppresses various deprecation warnings within the test suite from the API. --- build.gradle | 5 + sqlcipher/build.gradle | 4 + .../database/SQLCipherWALTestScenario.java | 5 +- .../database_cts/AbstractCursorTest.java | 56 +++++++-- .../database_cts/CursorJoinerTest.java | 32 +++-- .../database_cts/CursorWindowTest.java | 32 ++++- .../database_cts/CursorWrapperTest.java | 49 ++++++-- .../database_cts/DatabaseCursorTest.java | 92 +++++++------- ...ava => DatabaseUtilsInsertHelperTest.java} | 68 +++++++---- .../database_cts/DatabaseUtilsTest.java | 79 ++++++++---- .../database_cts/MergeCursorTest.java | 37 ++++-- .../DatabaseGeneralTest.java | 93 +++++++------- .../sqlcipher_cts/DatabaseStatementTest.java | 57 +++++---- .../SQLiteAbortExceptionTest.java | 11 +- .../sqlcipher_cts/SQLiteClosableTest.java | 17 ++- .../SQLiteConstraintExceptionTest.java | 11 +- .../sqlcipher_cts/SQLiteCursorTest.java | 43 +++++-- .../SQLiteDatabaseCorruptExceptionTest.java | 11 +- .../sqlcipher_cts/SQLiteDatabaseTest.java | 87 ++++++++++--- .../SQLiteDiskIOExceptionTest.java | 11 +- .../SQLiteDoneExceptionTest.java | 11 +- .../sqlcipher_cts/SQLiteExceptionTest.java | 11 +- .../database/sqlcipher_cts/SQLiteFtsTest.java | 37 ++++-- .../SQLiteFullExceptionTest.java | 11 +- .../SQLiteMisuseExceptionTest.java | 11 +- .../sqlcipher_cts/SQLiteOpenHelperTest.java | 28 ++++- .../sqlcipher_cts/SQLiteProgramTest.java | 39 ++++-- .../sqlcipher_cts/SQLiteQueryBuilderTest.java | 115 +++++++++++------- .../sqlcipher_cts/SQLiteStatementTest.java | 53 ++++++-- .../net/zetetic/database/AbstractCursor.java | 2 + .../database/sqlcipher/SQLiteGlobal.java | 1 + 31 files changed, 795 insertions(+), 324 deletions(-) rename sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/{DatabaseUtils_InsertHelperTest.java => DatabaseUtilsInsertHelperTest.java} (87%) diff --git a/build.gradle b/build.gradle index c74042c..882915d 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,11 @@ allprojects { mavenCentral() google() } + tasks.withType(JavaCompile).tap { + configureEach { + options.compilerArgs << "-Xlint:deprecation" + } + } } project.ext { diff --git a/sqlcipher/build.gradle b/sqlcipher/build.gradle index b1c7a29..4c65497 100644 --- a/sqlcipher/build.gradle +++ b/sqlcipher/build.gradle @@ -12,6 +12,10 @@ android { versionName "${rootProject.ext.libraryVersion}" project.archivesBaseName = "sqlcipher-android-${versionName}" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + // The following argument makes the Android Test Orchestrator run its + // "pm clear" command after each test invocation. This command ensures + // that the app's state is completely cleared between tests. + testInstrumentationRunnerArguments clearPackageData: 'true' consumerProguardFile 'consumer-rules.pro' } diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/SQLCipherWALTestScenario.java b/sqlcipher/src/androidTest/java/net/zetetic/database/SQLCipherWALTestScenario.java index 53a28e9..1540587 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/SQLCipherWALTestScenario.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/SQLCipherWALTestScenario.java @@ -1,5 +1,7 @@ package net.zetetic.database; +import static org.junit.Assert.assertEquals; + import android.content.Context; import android.database.Cursor; import android.os.AsyncTask; @@ -83,6 +85,7 @@ public void setup() throws Exception { } @Test + @SuppressWarnings("deprecation") public void testEncryptedWalMode() throws Exception { // create database final MyHelper helper = new MyHelper(mContext); @@ -91,7 +94,7 @@ public void testEncryptedWalMode() throws Exception { // verify that WAL journal mode is set final Cursor pragmaCursor = helper.getWritableDatabase().rawQuery("PRAGMA journal_mode", null); pragmaCursor.moveToFirst(); - Assert.assertEquals("wal", pragmaCursor.getString(pragmaCursor.getColumnIndex("journal_mode"))); + assertEquals("wal", pragmaCursor.getString(pragmaCursor.getColumnIndex("journal_mode"))); pragmaCursor.close(); // start long running transaction diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java index f89a267..230af5a 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/AbstractCursorTest.java @@ -16,6 +16,13 @@ package net.zetetic.database.database_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.Context; import android.database.CharArrayBuffer; import android.database.ContentObserver; @@ -28,10 +35,16 @@ import android.net.Uri; import android.os.Bundle; import android.provider.Settings; -import android.test.InstrumentationTestCase; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.Suppress; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; import java.util.ArrayList; import java.util.Random; @@ -39,7 +52,8 @@ /** * Test {@link AbstractCursor}. */ -public class AbstractCursorTest extends InstrumentationTestCase { +@RunWith(AndroidJUnit4.class) +public class AbstractCursorTest { private static final int POSITION0 = 0; private static final int POSITION1 = 1; private static final int ROW_MAX = 10; @@ -56,31 +70,31 @@ public class AbstractCursorTest extends InstrumentationTestCase { private File mDatabaseFile; private AbstractCursor mDatabaseCursor; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); setupDatabase(); ArrayList list = createTestList(ROW_MAX, COLUMN_NAMES.length); mTestAbstractCursor = new TestAbstractCursor(COLUMN_NAMES, list); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabaseCursor.close(); mTestAbstractCursor.close(); mDatabase.close(); if (mDatabaseFile.exists()) { mDatabaseFile.delete(); } - super.tearDown(); } + @Test public void testConstructor() { TestAbstractCursor abstractCursor = new TestAbstractCursor(); assertEquals(-1, abstractCursor.getPosition()); } + @Test public void testGetBlob() { try { mTestAbstractCursor.getBlob(0); @@ -90,6 +104,7 @@ public void testGetBlob() { } } + @Test public void testRegisterDataSetObserver() { MockDataSetObserver datasetObserver = new MockDataSetObserver(); @@ -113,6 +128,7 @@ public void testRegisterDataSetObserver() { mDatabaseCursor.registerDataSetObserver(datasetObserver); } + @Test public void testRegisterContentObserver() { MockContentObserver contentObserver = new MockContentObserver(); @@ -136,12 +152,14 @@ public void testRegisterContentObserver() { mDatabaseCursor.registerContentObserver(contentObserver); } + @Test public void testSetNotificationUri() { final Uri testUri = Settings.System.getUriFor(Settings.System.TIME_12_24); - mDatabaseCursor.setNotificationUri(getInstrumentation().getContext().getContentResolver(), + mDatabaseCursor.setNotificationUri(ApplicationProvider.getApplicationContext().getContentResolver(), testUri); } + @Test public void testRespond() { Bundle b = new Bundle(); Bundle bundle = mDatabaseCursor.respond(b); @@ -151,6 +169,7 @@ public void testRespond() { assertSame(Bundle.EMPTY, bundle); } + @Test public void testRequery() { MockDataSetObserver mock = new MockDataSetObserver(); mDatabaseCursor.registerDataSetObserver(mock); @@ -159,6 +178,7 @@ public void testRequery() { assertTrue(mock.hadCalledOnChanged()); } + @Test public void testOnChange() throws InterruptedException { MockContentObserver mock = new MockContentObserver(); mTestAbstractCursor.registerContentObserver(mock); @@ -173,6 +193,7 @@ public void testOnChange() throws InterruptedException { } @Suppress + @Test public void testOnMove() { assertFalse(mTestAbstractCursor.getOnMoveRet()); mTestAbstractCursor.moveToFirst(); @@ -187,6 +208,7 @@ public void testOnMove() { } @Suppress + @Test public void testOnMove_samePosition() { mTestAbstractCursor.moveToFirst(); mTestAbstractCursor.moveToPosition(5); @@ -197,6 +219,7 @@ public void testOnMove_samePosition() { assertEquals(6, mTestAbstractCursor.getRowsMovedSum()); } + @Test public void testMoveToPrevious() { // Test moveToFirst, isFirst, moveToNext, getPosition assertTrue(mDatabaseCursor.moveToFirst()); @@ -267,24 +290,29 @@ public void testMoveToPrevious() { assertFalse(mDatabaseCursor.isAfterLast()); } + @Test public void testIsClosed() { assertFalse(mDatabaseCursor.isClosed()); mDatabaseCursor.close(); assertTrue(mDatabaseCursor.isClosed()); } + @Test public void testGetWantsAllOnMoveCalls() { assertFalse(mDatabaseCursor.getWantsAllOnMoveCalls()); } + @Test public void testGetExtras() { assertSame(Bundle.EMPTY, mDatabaseCursor.getExtras()); } + @Test public void testGetCount() { assertEquals(DATA_COUNT, mDatabaseCursor.getCount()); } + @Test public void testGetColumnNames() { String[] names = mDatabaseCursor.getColumnNames(); assertEquals(COLUMN_NAMES1.length, names.length); @@ -294,11 +322,13 @@ public void testGetColumnNames() { } } + @Test public void testGetColumnName() { assertEquals(COLUMN_NAMES1[0], mDatabaseCursor.getColumnName(0)); assertEquals(COLUMN_NAMES1[1], mDatabaseCursor.getColumnName(1)); } + @Test public void testGetColumnIndexOrThrow() { final String COLUMN_FAKE = "fake_name"; assertEquals(POSITION0, mDatabaseCursor.getColumnIndex(COLUMN_NAMES1[POSITION0])); @@ -314,15 +344,18 @@ public void testGetColumnIndexOrThrow() { } } + @Test public void testGetColumnIndex() { assertEquals(POSITION0, mDatabaseCursor.getColumnIndex(COLUMN_NAMES1[POSITION0])); assertEquals(POSITION1, mDatabaseCursor.getColumnIndex(COLUMN_NAMES1[POSITION1])); } + @Test public void testGetColumnCount() { assertEquals(COLUMN_NAMES1.length, mDatabaseCursor.getColumnCount()); } + @Test public void testDeactivate() { MockDataSetObserver mock = new MockDataSetObserver(); mDatabaseCursor.registerDataSetObserver(mock); @@ -332,6 +365,7 @@ public void testDeactivate() { } @Suppress + @Test public void testCopyStringToBuffer() { CharArrayBuffer ca = new CharArrayBuffer(1000); mTestAbstractCursor.moveToFirst(); @@ -347,6 +381,7 @@ public void testCopyStringToBuffer() { } @Suppress + @Test public void testCheckPosition() { // Test with position = -1. try { @@ -370,6 +405,7 @@ public void testCheckPosition() { } } + @Test public void testSetExtras() { Bundle b = new Bundle(); mTestAbstractCursor.setExtras(b); @@ -396,7 +432,7 @@ private static ArrayList createTestList(int rows, int cols) { } private void setupDatabase() { - File dbDir = getInstrumentation().getTargetContext().getDir("tests", + File dbDir = ApplicationProvider.getApplicationContext().getDir("tests", Context.MODE_PRIVATE); mDatabaseFile = new File(dbDir, "database_test.db"); if (mDatabaseFile.exists()) { diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorJoinerTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorJoinerTest.java index 7911ac1..494a386 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorJoinerTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorJoinerTest.java @@ -17,16 +17,29 @@ package net.zetetic.database.database_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.Context; import android.database.Cursor; import android.database.CursorJoiner; import android.database.CursorJoiner.Result; import net.zetetic.database.sqlcipher.SQLiteDatabase; -import android.test.AndroidTestCase; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.io.File; -public class CursorJoinerTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class CursorJoinerTest { private static final int TEST_ITEM_COUNT = 10; private static final int DEFAULT_TABLE1_VALUE_BEGINS = 1; @@ -44,20 +57,20 @@ public class CursorJoinerTest extends AndroidTestCase { private SQLiteDatabase mDatabase; private File mDatabaseFile; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); setupDatabase(); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); mDatabaseFile.delete(); - super.tearDown(); } + @Test + @SuppressWarnings("deprecation") public void testCursorJoinerAndIterator() { Cursor cursor1 = getCursor(TABLE_NAME_1, null, null); Cursor cursor2 = getCursor(TABLE_NAME_2, null, null); @@ -132,6 +145,7 @@ public void testCursorJoinerAndIterator() { closeCursor(cursor1); } + @Test public void testNext() { String[] columnNames = new String[] { "number" }; Cursor cursor1 = getCursor(TABLE_NAME_1, null, columnNames); @@ -205,7 +219,7 @@ private void initializeTables() { } private void setupDatabase() { - File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + File dbDir = ApplicationProvider.getApplicationContext().getDir("tests", Context.MODE_PRIVATE); mDatabaseFile = new File(dbDir, "database_test.db"); if (mDatabaseFile.exists()) { mDatabaseFile.delete(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java index 055dee0..02df1b4 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWindowTest.java @@ -16,20 +16,31 @@ package net.zetetic.database.database_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.database.CharArrayBuffer; import android.database.sqlite.SQLiteException; -import android.test.AndroidTestCase; + +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.CursorWindow; import net.zetetic.database.MatrixCursor; import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; import java.util.Random; -public class CursorWindowTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class CursorWindowTest { private static final String TEST_STRING = "Test String"; @@ -38,6 +49,7 @@ public void setUp() { System.loadLibrary("sqlcipher"); } + @Test public void testWriteCursorToWindow() throws Exception { // create cursor String[] colNames = new String[]{"_id", "name", "number", "profit"}; @@ -82,6 +94,7 @@ public void testWriteCursorToWindow() throws Exception { assertEquals(0, window.getNumRows()); } + @Test public void testNull() { CursorWindow window = getOneByOneWindow(); @@ -89,10 +102,11 @@ public void testNull() { assertTrue(window.putNull(0, 0)); assertNull(window.getString(0, 0)); assertEquals(0, window.getLong(0, 0)); - assertEquals(0.0, window.getDouble(0, 0)); + assertEquals(0.0, window.getDouble(0, 0), 0.001d); assertNull(window.getBlob(0, 0)); } + @Test public void testEmptyString() { CursorWindow window = getOneByOneWindow(); @@ -100,15 +114,17 @@ public void testEmptyString() { assertTrue(window.putString("", 0, 0)); assertEquals("", window.getString(0, 0)); assertEquals(0, window.getLong(0, 0)); - assertEquals(0.0, window.getDouble(0, 0)); + assertEquals(0.0, window.getDouble(0, 0), 0.001d); } + @Test public void testConstructors() { // Test constructor with 'true' input, and getStartPosition should return 0 CursorWindow cursorWindow = new CursorWindow(""); assertEquals(0, cursorWindow.getStartPosition()); } + @Test public void testDataStructureOperations() { CursorWindow cursorWindow = new CursorWindow(""); @@ -161,6 +177,7 @@ public void testDataStructureOperations() { } } + @Test public void testAccessDataValues() { final long NUMBER_LONG_INTEGER = (long) 0xaabbccddffL; final long NUMBER_INTEGER = (int) NUMBER_LONG_INTEGER; @@ -200,7 +217,7 @@ public void testAccessDataValues() { assertEquals(0, cursorWindow.getLong(0, 0)); assertEquals(0, cursorWindow.getInt(0, 0)); assertEquals(0, cursorWindow.getShort(0, 0)); - assertEquals(0.0, cursorWindow.getDouble(0, 0)); + assertEquals(0.0, cursorWindow.getDouble(0, 0), 0.001d); assertEquals(0.0f, cursorWindow.getFloat(0, 0), 0.00000001f); assertFalse(cursorWindow.isNull(0, 0)); assertFalse(cursorWindow.isBlob(0, 0)); @@ -212,7 +229,7 @@ public void testAccessDataValues() { assertEquals(0, cursorWindow.getLong(0, 1)); assertEquals(0, cursorWindow.getInt(0, 1)); assertEquals(0, cursorWindow.getShort(0, 1)); - assertEquals(0.0, cursorWindow.getDouble(0, 1)); + assertEquals(0.0, cursorWindow.getDouble(0, 1), 0.001d); assertEquals(0.0f, cursorWindow.getFloat(0, 1), 0.00000001f); assertNull(cursorWindow.getBlob(0, 1)); assertTrue(cursorWindow.isNull(0, 1)); @@ -265,6 +282,7 @@ public void testAccessDataValues() { assertTrue(cursorWindow.isBlob(0, 4)); } + @Test public void testCopyStringToBuffer() { int DEFAULT_ARRAY_LENGTH = 60; String baseString = "0123456789"; @@ -300,6 +318,7 @@ public void testCopyStringToBuffer() { assertEquals(expectedString.length(), charArrayBuffer.data.length); } + @Test public void testAccessStartPosition() { final int TEST_POSITION_1 = 0; final int TEST_POSITION_2 = 3; @@ -329,6 +348,7 @@ public void testAccessStartPosition() { } } + @Test public void testClearAndOnAllReferencesReleased() { MockCursorWindow cursorWindow = new MockCursorWindow(true); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWrapperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWrapperTest.java index 042905f..4372c2f 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWrapperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/CursorWrapperTest.java @@ -17,6 +17,13 @@ package net.zetetic.database.database_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.Context; import android.database.CharArrayBuffer; import android.database.ContentObserver; @@ -26,14 +33,22 @@ import android.database.StaleDataException; import android.os.Bundle; import android.os.Handler; -import android.test.AndroidTestCase; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.sqlcipher.SQLiteDatabase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; import java.util.Arrays; -public class CursorWrapperTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class CursorWrapperTest { private static final String FIRST_NUMBER = "123"; private static final String SECOND_NUMBER = "5555"; @@ -52,20 +67,20 @@ public class CursorWrapperTest extends AndroidTestCase { private static final int CURRENT_DATABASE_VERSION = 42; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); setupDatabase(); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { closeDatabase(); - super.tearDown(); } - public void testConstrucotorAndClose() { + @Test + @SuppressWarnings("deprecation") + public void testConstructorAndClose() { CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); assertTrue(cursorWrapper.requery()); @@ -85,6 +100,8 @@ private Cursor getCursor() { return cursor; } + @Test + @SuppressWarnings("deprecation") public void testGetCount() { CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); int defaultCount = cursorWrapper.getCount(); @@ -119,6 +136,8 @@ public void testGetCount() { rebuildDatabase(); } + @Test + @SuppressWarnings("deprecation") public void testDeactivate() throws IllegalStateException { CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); MockObserver observer = new MockObserver(); @@ -175,6 +194,7 @@ public void testDeactivate() throws IllegalStateException { } } + @Test public void testGettingColumnInfos() { CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); @@ -205,6 +225,7 @@ public void testGettingColumnInfos() { cursorWrapper.close(); } + @Test public void testPositioning() { CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); @@ -282,6 +303,7 @@ public void testPositioning() { cursorWrapper.close(); } + @Test public void testGettingValues() { final byte NUMBER_BLOB_UNIT = 99; final String STRING_TEXT = "Test String"; @@ -358,12 +380,14 @@ public void testGettingValues() { mDatabase.execSQL("DROP TABLE test2"); } + @Test public void testGetExtras() { CursorWrapper cursor = new CursorWrapper(getCursor()); Bundle bundle = cursor.getExtras(); assertSame(Bundle.EMPTY, bundle); } + @Test public void testCopyStringToBuffer() { CharArrayBuffer charArrayBuffer = new CharArrayBuffer(1000); Cursor cursor = getCursor(); @@ -390,6 +414,7 @@ public void testCopyStringToBuffer() { cursorWrapper.close(); } + @Test public void testRespond() { Bundle b = new Bundle(); CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); @@ -398,12 +423,15 @@ public void testRespond() { cursorWrapper.close(); } + @Test public void testGetWantsAllOnMoveCalls() { CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); assertFalse(cursorWrapper.getWantsAllOnMoveCalls()); cursorWrapper.close(); } + + @Test public void testContentObserverOperations() throws IllegalStateException { CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); MockContentObserver observer = new MockContentObserver(null); @@ -439,6 +467,7 @@ public void testContentObserverOperations() throws IllegalStateException { cursorWrapper.close(); } + @Test public void testSetExtras() { if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){ Cursor cursor = getCursor(); @@ -473,7 +502,7 @@ private void deleteAllRecords(SQLiteDatabase database) { } private void setupDatabase() { - File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + File dbDir = ApplicationProvider.getApplicationContext().getDir("tests", Context.MODE_PRIVATE); /* don't use the same database name as the one in super class * this class's setUp() method deletes a database file just opened by super.setUp(). * that can cause corruption in database in the following situation: diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseCursorTest.java index 0985c94..4d161d1 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseCursorTest.java @@ -16,6 +16,13 @@ package net.zetetic.database.database_cts; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -28,17 +35,22 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteQuery; import android.os.Looper; -import android.test.AndroidTestCase; -import android.test.PerformanceTestCase; -import android.test.suitebuilder.annotation.LargeTest; -import android.test.suitebuilder.annotation.MediumTest; import android.util.Log; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; import java.util.Arrays; import java.util.Random; -public class DatabaseCursorTest extends AndroidTestCase implements PerformanceTestCase { +@RunWith(AndroidJUnit4.class) +public class DatabaseCursorTest { private static final String sString1 = "this is a test"; private static final String sString2 = "and yet another test"; private static final String sString3 = "this string is a little longer, but still a test"; @@ -49,12 +61,13 @@ public class DatabaseCursorTest extends AndroidTestCase implements PerformanceTe protected static final int TYPE_CURSOR = 0; protected static final int TYPE_CURSORWRAPPER = 1; private int mTestType = TYPE_CURSOR; + private Context mContext; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { + mContext = ApplicationProvider.getApplicationContext(); System.loadLibrary("sqlcipher"); - File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + File dbDir = mContext.getDir("tests", Context.MODE_PRIVATE); mDatabaseFile = new File(dbDir, "database_test.db"); if (mDatabaseFile.exists()) { mDatabaseFile.delete(); @@ -64,11 +77,10 @@ protected void setUp() throws Exception { mDatabase.setVersion(CURRENT_DATABASE_VERSION); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); mDatabaseFile.delete(); - super.tearDown(); } public void setupTestType(int testType) { @@ -89,11 +101,6 @@ public boolean isPerformanceOnly() { return false; } - // These test can only be run once. - public int startPerformance(Intermediates intermediates) { - return 1; - } - private void populateDefaultTable() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); @@ -102,7 +109,7 @@ private void populateDefaultTable() { mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString3 + "');"); } - @MediumTest + @Test public void testBlob() throws Exception { // create table mDatabase.execSQL( @@ -137,13 +144,13 @@ public void testBlob() throws Exception { int dCol = testCursor.getColumnIndexOrThrow("d"); int lCol = testCursor.getColumnIndexOrThrow("l"); byte[] cBlob = testCursor.getBlob(bCol); - assertTrue(Arrays.equals(blob, cBlob)); + assertArrayEquals(blob, cBlob); assertEquals(s, testCursor.getString(sCol)); - assertEquals((double) d, testCursor.getDouble(dCol)); + assertEquals(d, testCursor.getDouble(dCol), 0.001d); assertEquals((long) l, testCursor.getLong(lCol)); } - @MediumTest + @Test public void testRealColumns() throws Exception { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data REAL);"); ContentValues values = new ContentValues(); @@ -153,11 +160,11 @@ public void testRealColumns() throws Exception { Cursor testCursor = getTestCursor(mDatabase.rawQuery("SELECT data FROM test", null)); assertNotNull(testCursor); assertTrue(testCursor.moveToFirst()); - assertEquals(42.11, testCursor.getDouble(0)); + assertEquals(42.11, testCursor.getDouble(0), 0.001d); testCursor.close(); } - @MediumTest + @Test public void testCursor1() throws Exception { populateDefaultTable(); @@ -206,9 +213,8 @@ public void testCursor1() throws Exception { assertEquals(sString3, s); int i; - - for (testCursor.moveToFirst(), i = 0; !testCursor.isAfterLast(); - testCursor.moveToNext(), i++) { + testCursor.moveToFirst(); + for (i = 0; !testCursor.isAfterLast(); testCursor.moveToNext(), i++) { testCursor.getInt(0); } @@ -223,7 +229,7 @@ public void testCursor1() throws Exception { testCursor.close(); } - @MediumTest + @Test public void testCursor2() throws Exception { populateDefaultTable(); @@ -240,8 +246,8 @@ public void testCursor2() throws Exception { } int i; - for (testCursor.moveToFirst(), i = 0; !testCursor.isAfterLast(); - testCursor.moveToNext(), i++) { + testCursor.moveToFirst(); + for (i = 0; !testCursor.isAfterLast(); testCursor.moveToNext(), i++) { testCursor.getInt(0); } assertEquals(0, i); @@ -254,7 +260,7 @@ public void testCursor2() throws Exception { testCursor.close(); } - @MediumTest + @Test public void testLargeField() throws Exception { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); @@ -312,7 +318,7 @@ public void onInvalidated() { } } - @LargeTest + @Test public void testManyRowsLong() throws Exception { mDatabase.beginTransaction(); final int count = 9000; @@ -343,7 +349,7 @@ public void testManyRowsLong() throws Exception { testCursor.close(); } - @LargeTest + @Test public void testManyRowsTxt() throws Exception { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); StringBuilder sql = new StringBuilder(2100); @@ -376,7 +382,7 @@ public void testManyRowsTxt() throws Exception { testCursor.close(); } - @LargeTest + @Test public void testManyRowsTxtLong() throws Exception { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, txt TEXT, data INT);"); @@ -413,8 +419,9 @@ public void testManyRowsTxtLong() throws Exception { testCursor.close(); } - @MediumTest - public void testRequery() throws Exception { + @Test + @SuppressWarnings("deprecation") + public void testReQuery() throws Exception { populateDefaultTable(); Cursor testCursor = getTestCursor(mDatabase.rawQuery("SELECT * FROM test", null)); @@ -426,8 +433,9 @@ public void testRequery() throws Exception { testCursor.close(); } - @MediumTest - public void testRequeryWithSelection() throws Exception { + @Test + @SuppressWarnings("deprecation") + public void testReQueryWithSelection() throws Exception { populateDefaultTable(); Cursor testCursor = getTestCursor( @@ -445,8 +453,9 @@ public void testRequeryWithSelection() throws Exception { testCursor.close(); } - @MediumTest - public void testRequeryWithSelectionArgs() throws Exception { + @Test + @SuppressWarnings("deprecation") + public void testReQueryWithSelectionArgs() throws Exception { populateDefaultTable(); Cursor testCursor = getTestCursor(mDatabase.rawQuery("SELECT data FROM test WHERE data = ?", @@ -463,8 +472,9 @@ public void testRequeryWithSelectionArgs() throws Exception { testCursor.close(); } - @MediumTest - public void testRequeryWithAlteredSelectionArgs() throws Exception { + @Test + @SuppressWarnings("deprecation") + public void testReQueryWithAlteredSelectionArgs() throws Exception { /** * Test the ability of a subclass of SQLiteCursor to change its query arguments. */ diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtils_InsertHelperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsInsertHelperTest.java similarity index 87% rename from sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtils_InsertHelperTest.java rename to sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsInsertHelperTest.java index ba990f7..1d97d19 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtils_InsertHelperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsInsertHelperTest.java @@ -17,49 +17,69 @@ package net.zetetic.database.database_cts; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; import net.zetetic.database.DatabaseUtils.InsertHelper; import net.zetetic.database.sqlcipher.SQLiteDatabase; -import android.test.AndroidTestCase; -import android.test.MoreAsserts; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.io.File; +import java.util.Arrays; -public class DatabaseUtils_InsertHelperTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +@SuppressWarnings("deprecation") +public class DatabaseUtilsInsertHelperTest { private static final String TEST_TABLE_NAME = "test"; private static final String DATABASE_NAME = "database_test.db"; private SQLiteDatabase mDatabase; private InsertHelper mInsertHelper; + private Context mContext; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { + mContext = ApplicationProvider.getApplicationContext(); System.loadLibrary("sqlcipher"); - getContext().deleteDatabase(DATABASE_NAME); + mContext.deleteDatabase(DATABASE_NAME); File f = mContext.getDatabasePath(DATABASE_NAME); mDatabase = SQLiteDatabase.openOrCreateDatabase(f, null); assertNotNull(mDatabase); mInsertHelper = new InsertHelper(mDatabase, TEST_TABLE_NAME); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mInsertHelper.close(); mDatabase.close(); - getContext().deleteDatabase(DATABASE_NAME); - super.tearDown(); + mContext.deleteDatabase(DATABASE_NAME); } + @Test public void testConstructor() { new InsertHelper(mDatabase, TEST_TABLE_NAME); } + @Test public void testClose() { mInsertHelper.close(); } + @Test public void testGetColumnIndex() { mDatabase.execSQL("CREATE TABLE " + TEST_TABLE_NAME + " (_id INTEGER PRIMARY KEY, " + "name TEXT, age INTEGER, address TEXT);"); @@ -75,6 +95,7 @@ public void testGetColumnIndex() { } } + @Test public void testInsert() { mDatabase.execSQL("CREATE TABLE " + TEST_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + " boolean_value INTEGER, int_value INTEGER, long_value INTEGER," + @@ -132,11 +153,11 @@ public void testInsert() { assertEquals(1, cursor.getInt(booleanValueIndex)); assertEquals(10, cursor.getInt(intValueIndex)); assertEquals(1000L, cursor.getLong(longValueIndex)); - assertEquals(123.456, cursor.getDouble(doubleValueIndex)); - assertEquals(1.0f, cursor.getFloat(floatValueIndex)); + assertEquals(123.456d, cursor.getDouble(doubleValueIndex), 0.001d); + assertEquals(1.0f, cursor.getFloat(floatValueIndex), 0.001d); assertEquals("test insert", cursor.getString(stringValueIndex)); byte[] value = cursor.getBlob(blobValueIndex); - MoreAsserts.assertEquals(blob, value); + assertTrue(Arrays.equals(blob, value)); assertNull(cursor.getString(nullValueIndex)); cursor.close(); @@ -164,11 +185,11 @@ public void testInsert() { assertEquals(0, cursor.getInt(booleanValueIndex)); assertEquals(123, cursor.getInt(intValueIndex)); assertEquals(987654L, cursor.getLong(longValueIndex)); - assertEquals(654.321, cursor.getDouble(doubleValueIndex)); - assertEquals(21.1f, cursor.getFloat(floatValueIndex)); + assertEquals(654.321d, cursor.getDouble(doubleValueIndex), 0.001d); + assertEquals(21.1f, cursor.getFloat(floatValueIndex), 0.001d); assertEquals("insert another row", cursor.getString(stringValueIndex)); value = cursor.getBlob(blobValueIndex); - MoreAsserts.assertEquals(blob, value); + assertTrue(Arrays.equals(blob, value)); assertNull(cursor.getString(nullValueIndex)); cursor.close(); @@ -177,6 +198,7 @@ public void testInsert() { assertEquals(-1, mInsertHelper.insert(values)); } + @Test public void testReplace() { mDatabase.execSQL("CREATE TABLE " + TEST_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + " boolean_value INTEGER, int_value INTEGER, long_value INTEGER," + @@ -228,11 +250,11 @@ public void testReplace() { assertEquals(1, cursor.getInt(booleanValueIndex)); assertEquals(10, cursor.getInt(intValueIndex)); assertEquals(1000L, cursor.getLong(longValueIndex)); - assertEquals(123.456, cursor.getDouble(doubleValueIndex)); - assertEquals(1.0f, cursor.getFloat(floatValueIndex)); + assertEquals(123.456, cursor.getDouble(doubleValueIndex), .001d); + assertEquals(1.0f, cursor.getFloat(floatValueIndex), 0.001d); assertEquals("test insert", cursor.getString(stringValueIndex)); byte[] value = cursor.getBlob(blobValueIndex); - MoreAsserts.assertEquals(blob, value); + assertTrue(Arrays.equals(blob, value)); assertNull(cursor.getString(nullValueIndex)); cursor.close(); @@ -274,11 +296,11 @@ public void testReplace() { assertEquals(0, cursor.getInt(booleanValueIndex)); assertEquals(123, cursor.getInt(intValueIndex)); assertEquals(987654L, cursor.getLong(longValueIndex)); - assertEquals(654.321, cursor.getDouble(doubleValueIndex)); - assertEquals(21.1f, cursor.getFloat(floatValueIndex)); + assertEquals(654.321d, cursor.getDouble(doubleValueIndex), 0.001d); + assertEquals(21.1f, cursor.getFloat(floatValueIndex), 0.001d); assertEquals("replace the row", cursor.getString(stringValueIndex)); value = cursor.getBlob(blobValueIndex); - MoreAsserts.assertEquals(blob, value); + assertArrayEquals(blob, value); assertNull(cursor.getString(nullValueIndex)); cursor.close(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsTest.java index 5fc903a..a130cf1 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/DatabaseUtilsTest.java @@ -17,27 +17,43 @@ package net.zetetic.database.database_cts; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteException; import android.os.ParcelFileDescriptor; -import android.test.AndroidTestCase; -import android.test.MoreAsserts; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.DatabaseUtils; import net.zetetic.database.DatabaseUtils.InsertHelper; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteStatement; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; -public class DatabaseUtilsTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class DatabaseUtilsTest { private SQLiteDatabase mDatabase; private File mDatabaseFile; private static final String[] TEST_PROJECTION = new String[] { @@ -47,12 +63,13 @@ public class DatabaseUtilsTest extends AndroidTestCase { "address" // 3 }; private static final String TABLE_NAME = "test"; + private Context mContext; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { + mContext = ApplicationProvider.getApplicationContext(); System.loadLibrary("sqlcipher"); - File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + File dbDir = mContext.getDir("tests", Context.MODE_PRIVATE); mDatabaseFile = new File(dbDir, "database_test.db"); if (mDatabaseFile.exists()) { mDatabaseFile.delete(); @@ -67,13 +84,13 @@ protected void setUp() throws Exception { "CREATE TABLE boolean_test (_id INTEGER PRIMARY KEY, value BOOLEAN)"); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); mDatabaseFile.delete(); - super.tearDown(); } + @Test public void testAppendEscapedSQLString() { String expected = "name='Mike'"; StringBuilder sb = new StringBuilder("name="); @@ -86,11 +103,13 @@ public void testAppendEscapedSQLString() { assertEquals(expected, sb.toString()); } + @Test public void testSqlEscapeString() { String expected = "'Jack'"; assertEquals(expected, DatabaseUtils.sqlEscapeString("Jack")); } + @Test public void testAppendValueToSql() { String expected = "address='LA'"; StringBuilder sb = new StringBuilder("address="); @@ -108,6 +127,7 @@ public void testAppendValueToSql() { assertEquals(expected, sb.toString()); } + @Test public void testBindObjectToProgram() { String name = "Mike"; int age = 21; @@ -136,6 +156,7 @@ public void testBindObjectToProgram() { assertEquals(address, cursor.getString(3)); } + @Test public void testCreateDbFromSqlStatements() { String dbName = "ExampleName"; String sqls = "CREATE TABLE " + TABLE_NAME + " (_id INTEGER PRIMARY KEY, name TEXT);\n" @@ -143,7 +164,7 @@ public void testCreateDbFromSqlStatements() { File f = mContext.getDatabasePath(dbName); f.mkdirs(); f.delete(); - DatabaseUtils.createDbFromSqlStatements(getContext(), f.toString(), 1, sqls); + DatabaseUtils.createDbFromSqlStatements(mContext, f.toString(), 1, sqls); SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f,null); final String[] PROJECTION = new String[] { @@ -155,9 +176,10 @@ public void testCreateDbFromSqlStatements() { assertEquals(1, cursor.getCount()); cursor.moveToFirst(); assertEquals("Mike", cursor.getString(1)); - getContext().deleteDatabase(dbName); + mContext.deleteDatabase(dbName); } + @Test public void testCursorDoubleToContentValues() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -169,15 +191,16 @@ public void testCursorDoubleToContentValues() { String key = "key"; cursor.moveToFirst(); DatabaseUtils.cursorDoubleToContentValues(cursor, "age", contentValues, key); - assertEquals(20.0, contentValues.getAsDouble(key)); + assertEquals(20.0, contentValues.getAsDouble(key), 0.001d); DatabaseUtils.cursorDoubleToContentValues(cursor, "Error Field Name", contentValues, key); assertNull(contentValues.getAsDouble(key)); DatabaseUtils.cursorDoubleToContentValues(cursor, "name", contentValues, key); - assertEquals(0.0, contentValues.getAsDouble(key)); + assertEquals(0.0, contentValues.getAsDouble(key), 0.001d); } + @Test public void testCursorDoubleToCursorValues() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -188,15 +211,16 @@ public void testCursorDoubleToCursorValues() { ContentValues contentValues = new ContentValues(); cursor.moveToFirst(); DatabaseUtils.cursorDoubleToCursorValues(cursor, "age", contentValues); - assertEquals(20.0, contentValues.getAsDouble("age")); + assertEquals(20.0, contentValues.getAsDouble("age"), 0.001d); DatabaseUtils.cursorDoubleToCursorValues(cursor, "Error Field Name", contentValues); assertNull(contentValues.getAsDouble("Error Field Name")); DatabaseUtils.cursorDoubleToCursorValues(cursor, "name", contentValues); - assertEquals(0.0, contentValues.getAsDouble("name")); + assertEquals(0.0, contentValues.getAsDouble("name"), 0.001d); } + @Test public void testCursorIntToContentValues() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -226,7 +250,8 @@ public void testCursorIntToContentValues() { assertEquals(Integer.valueOf(0), contentValues.getAsInteger("name")); } - public void testcursorLongToContentValues() { + @Test + public void testCursorLongToContentValues() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); @@ -255,6 +280,7 @@ public void testcursorLongToContentValues() { assertEquals(Long.valueOf(0), contentValues.getAsLong("name")); } + @Test public void testCursorRowToContentValues() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -281,16 +307,17 @@ public void testCursorRowToContentValues() { cursor.moveToNext(); DatabaseUtils.cursorRowToContentValues(cursor, contentValues); assertFalse(contentValues.getAsBoolean("value")); - assertTrue(contentValues.getAsInteger("value")==0); + assertEquals(0, (int) contentValues.getAsInteger("value")); cursor.moveToNext(); DatabaseUtils.cursorRowToContentValues(cursor, contentValues); - assertTrue(contentValues.getAsInteger("value")==1); + assertEquals(1, (int) contentValues.getAsInteger("value")); // The following does not work: // https://stackoverflow.com/questions/13095277/android-database-with-contentvalue-with-boolean-bug // assertTrue(contentValues.getAsBoolean("value")); } + @Test public void testCursorStringToContentValues() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -330,6 +357,8 @@ public void testCursorStringToContentValues() { assertEquals("Mike", contentValues.get("name")); } + @Test + @SuppressWarnings("deprecation") public void testCursorStringToInsertHelper() { // create a new table. mDatabase.execSQL("CREATE TABLE test_copy (_id INTEGER PRIMARY KEY, " + @@ -361,6 +390,7 @@ public void testCursorStringToInsertHelper() { assertEquals("LA", cursor.getString(3)); } + @Test public void testDumpCurrentRow() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -386,6 +416,7 @@ public void testDumpCurrentRow() { assertEquals(expected, DatabaseUtils.dumpCurrentRowToString(cursor)); } + @Test public void testDumpCursor() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -429,6 +460,7 @@ public void testDumpCursor() { assertEquals(pos, cursor.getPosition()); // dumpCursor should not change status of cursor } + @Test public void testCollationKey() { String key1 = DatabaseUtils.getCollationKey("abc"); String key2 = DatabaseUtils.getCollationKey("ABC"); @@ -441,10 +473,11 @@ public void testCollationKey() { key2 = DatabaseUtils.getHexCollationKey("ABC"); key3 = DatabaseUtils.getHexCollationKey("bcd"); - assertTrue(key1.equals(key2)); - assertFalse(key1.equals(key3)); + assertEquals(key1, key2); + assertNotEquals(key1, key3); } + @Test public void testLongForQuery() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -484,6 +517,7 @@ public void testLongForQuery() { statement.close(); } + @Test public void testQueryNumEntries() { assertEquals(0, DatabaseUtils.queryNumEntries(mDatabase, TABLE_NAME)); @@ -557,6 +591,7 @@ public void testQueryNumEntries() { // } // } + @Test public void testStringForQuery() { mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + " VALUES ('Mike', '20', 'LA');"); @@ -659,7 +694,7 @@ private static void assertInputStreamContent(byte[] expected, InputStream is) int count = is.read(observed); assertEquals(expected.length, count); assertEquals(-1, is.read()); - MoreAsserts.assertEquals(expected, observed); + assertArrayEquals(expected, observed); } finally { is.close(); } diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/MergeCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/MergeCursorTest.java index 7c43778..4671c7d 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/MergeCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/database_cts/MergeCursorTest.java @@ -17,6 +17,12 @@ package net.zetetic.database.database_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; @@ -24,12 +30,20 @@ import android.database.MergeCursor; import android.database.StaleDataException; import net.zetetic.database.sqlcipher.SQLiteDatabase; -import android.test.AndroidTestCase; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.io.File; import java.util.Arrays; -public class MergeCursorTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class MergeCursorTest { private final int NUMBER_1_COLUMN_INDEX = 1; private static final String TABLE1_NAME = "test1"; private static final String TABLE2_NAME = "test2"; @@ -54,16 +68,15 @@ public class MergeCursorTest extends AndroidTestCase { private static final int MAX_VALUE = 10; private static final int HALF_VALUE = MAX_VALUE / 2; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); setupDatabase(); mCursors = new Cursor[2]; } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { for (int i = 0; i < mCursors.length; i++) { if (null != mCursors[i]) { mCursors[i].close(); @@ -71,9 +84,9 @@ protected void tearDown() throws Exception { } mDatabase.close(); mDatabaseFile.delete(); - super.tearDown(); } + @Test public void testConstructor() { // If each item of mCursors are null, count will be zero. MergeCursor mergeCursor = new MergeCursor(mCursors); @@ -86,6 +99,7 @@ public void testConstructor() { assertEquals(mCursors[0].getCount() + mCursors[1].getCount(), mergeCursor.getCount()); } + @Test public void testOnMove() { createCursors(); MergeCursor mergeCursor = new MergeCursor(mCursors); @@ -97,6 +111,7 @@ public void testOnMove() { } } + @Test public void testCursorSwiching() { mDatabase.execSQL("CREATE TABLE " + TABLE5_NAME + " (_id INTEGER PRIMARY KEY," + TABLE3_COLUMNS + ");"); @@ -124,6 +139,7 @@ public void testCursorSwiching() { assertTrue(Arrays.equals(tableColumns, mergeCursor.getColumnNames())); } + @Test public void testGetValues() { byte NUMBER_BLOB_UNIT = 99; String[] TEST_STRING = new String[] {"Test String1", "Test String2"}; @@ -183,6 +199,7 @@ public void testGetValues() { } } + @Test public void testContentObsererOperations() throws IllegalStateException { createCursors(); MergeCursor mergeCursor = new MergeCursor(mCursors); @@ -220,6 +237,7 @@ public void testContentObsererOperations() throws IllegalStateException { } } + @Test public void testDeactivate() throws IllegalStateException { createCursors(); MergeCursor mergeCursor = new MergeCursor(mCursors); @@ -281,6 +299,7 @@ public void testDeactivate() throws IllegalStateException { } } + @Test public void testRequery() { final String TEST_VALUE1 = Integer.toString(MAX_VALUE + 1); final String TEST_VALUE2 = Integer.toString(MAX_VALUE + 2); @@ -325,7 +344,7 @@ private void buildDatabaseWithTestValues(String text, double doubleNumber, long } private void setupDatabase() { - File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + File dbDir = ApplicationProvider.getApplicationContext().getDir("tests", Context.MODE_PRIVATE); mDatabaseFile = new File(dbDir, "database_test.db"); if (mDatabaseFile.exists()) { mDatabaseFile.delete(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java index 38df84b..12cd90e 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_android/DatabaseGeneralTest.java @@ -17,6 +17,16 @@ package net.zetetic.database.sqlcipher_android; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.ContentValues; import android.content.Context; import android.database.CharArrayBuffer; @@ -24,27 +34,34 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteException; import android.os.Parcel; -import android.test.AndroidTestCase; -import android.test.PerformanceTestCase; -import android.test.suitebuilder.annotation.LargeTest; -import android.test.suitebuilder.annotation.MediumTest; -import android.test.suitebuilder.annotation.SmallTest; -import android.test.suitebuilder.annotation.Suppress; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.LargeTest; +import androidx.test.filters.Suppress; + import android.util.Log; import android.util.Pair; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteStatement; import net.zetetic.database.DefaultDatabaseErrorHandler; import junit.framework.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; -@SuppressWarnings({"deprecated", "ResultOfMethodCallIgnored"}) -public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceTestCase { +@RunWith(AndroidJUnit4.class) +public class DatabaseGeneralTest { private static final String TAG = "DatabaseGeneralTest"; private static final String sString1 = "this is a test"; @@ -56,36 +73,29 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT private SQLiteDatabase mDatabase; private File mDatabaseFile; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); - File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); + File dbDir = ApplicationProvider.getApplicationContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); mDatabaseFile = new File(dbDir, "database_test.db"); if (mDatabaseFile.exists()) { mDatabaseFile.delete(); } mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); - assertNotNull(mDatabase); + assertThat(mDatabase, is(notNullValue())); mDatabase.setVersion(CURRENT_DATABASE_VERSION); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); mDatabaseFile.delete(); - super.tearDown(); } public boolean isPerformanceOnly() { return false; } - // These test can only be run once. - public int startPerformance(Intermediates intermediates) { - return 1; - } - private void populateDefaultTable() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); @@ -94,7 +104,7 @@ private void populateDefaultTable() { mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString3 + "');"); } - @MediumTest + @Test public void testCustomFunctionNoReturn() { mDatabase.addCustomFunction("emptyFunction", 1, new SQLiteDatabase.CustomFunction() { @Override @@ -108,14 +118,14 @@ public void callback(String[] args) { assertSame(null, cursor.getString(0)); } - @MediumTest + @Test public void testVersion() throws Exception { assertEquals(CURRENT_DATABASE_VERSION, mDatabase.getVersion()); mDatabase.setVersion(11); assertEquals(11, mDatabase.getVersion()); } - @MediumTest + @Test public void testUpdate() throws Exception { populateDefaultTable(); @@ -131,7 +141,7 @@ public void testUpdate() throws Exception { } @Suppress // PHONE_NUMBERS_EQUAL not supported - @MediumTest + @Test public void testPhoneNumbersEqual() throws Exception { mDatabase.execSQL("CREATE TABLE phones (num TEXT);"); mDatabase.execSQL("INSERT INTO phones (num) VALUES ('911');"); @@ -281,7 +291,7 @@ private void assertPhoneNumberNotEqual(String phone1, String phone2, boolean use * @throws Exception */ @Suppress // PHONE_NUMBERS_EQUAL not supported - @SmallTest + @Test public void testPhoneNumbersEqualInternationl() throws Exception { assertPhoneNumberEqual("1", "1"); assertPhoneNumberEqual("123123", "123123"); @@ -337,7 +347,7 @@ public void testPhoneNumbersEqualInternationl() throws Exception { assertPhoneNumberNotEqual("080-1234-5678", "+819012345678", true); } - @MediumTest + @Test public void testCopyString() throws Exception { mDatabase.execSQL("CREATE TABLE guess (numi INTEGER, numf FLOAT, str TEXT);"); mDatabase.execSQL( @@ -378,8 +388,8 @@ public void testCopyString() throws Exception { c.moveToNext(); c.copyStringToBuffer(numfIdx, buf); - assertEquals(-1.0, Double.valueOf( - new String(buf.data, 0, buf.sizeCopied))); + var value = java.lang.Double.valueOf(new String(buf.data, 0, buf.sizeCopied)).doubleValue(); + assertEquals(-1.0d, value, 0.001); c.copyStringToBuffer(strIdx, buf); compareTo = c.getString(strIdx); @@ -389,7 +399,7 @@ public void testCopyString() throws Exception { c.close(); } - @MediumTest + @Test public void testSchemaChange1() throws Exception { SQLiteDatabase db1 = mDatabase; Cursor cursor; @@ -405,7 +415,7 @@ public void testSchemaChange1() throws Exception { cursor.close(); } - @MediumTest + @Test public void testSchemaChange2() throws Exception { mDatabase.execSQL("CREATE TABLE db1 (_id INTEGER PRIMARY KEY, data TEXT);"); Cursor cursor = mDatabase.query("db1", null, null, null, null, null, null); @@ -414,7 +424,7 @@ public void testSchemaChange2() throws Exception { cursor.close(); } - @MediumTest + @Test public void testSchemaChange3() throws Exception { mDatabase.execSQL("CREATE TABLE db1 (_id INTEGER PRIMARY KEY, data TEXT);"); mDatabase.execSQL("INSERT INTO db1 (data) VALUES ('test');"); @@ -431,7 +441,7 @@ public void testSchemaChange3() throws Exception { } } - @MediumTest + @Test public void testSelectionArgs() throws Exception { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); ContentValues values = new ContentValues(1); @@ -449,7 +459,7 @@ public void testSelectionArgs() throws Exception { } @Suppress // unicode collator not supported yet - @MediumTest + @Test public void testTokenize() throws Exception { Cursor c; mDatabase.execSQL("CREATE TABLE tokens (" + @@ -621,7 +631,7 @@ public void testTokenize() throws Exception { "SELECT source from tokens_no_index where token GLOB '" + key + "*'", null)); } - @MediumTest + @Test public void testTransactions() throws Exception { mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); @@ -724,7 +734,7 @@ private void assertThrowsIllegalState(Runnable r) { Assert.assertTrue(ok); } - @MediumTest + @Test public void testContentValues() throws Exception { ContentValues values = new ContentValues(); values.put("string", "value"); @@ -748,7 +758,7 @@ public void testContentValues() throws Exception { public static final int TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1; public static final int TABLE_INFO_PRAGMA_DEFAULT_INDEX = 4; - @MediumTest + @Test public void testTableInfoPragma() throws Exception { mDatabase.execSQL("CREATE TABLE pragma_test (" + "i INTEGER DEFAULT 1234, " + @@ -797,7 +807,7 @@ public void testTableInfoPragma() throws Exception { } } - @MediumTest + @Test public void testSemicolonsInStatements() throws Exception { mDatabase.execSQL("CREATE TABLE pragma_test (" + "i INTEGER DEFAULT 1234, " + @@ -817,7 +827,7 @@ public void testSemicolonsInStatements() throws Exception { } } - @MediumTest + @Test public void testUnionsWithBindArgs() { /* make sure unions with bindargs work http://b/issue?id=1061291 */ mDatabase.execSQL("CREATE TABLE A (i int);"); @@ -850,7 +860,7 @@ public void testUnionsWithBindArgs() { * It finishes without failure when it is not available. */ @Suppress - @MediumTest + @Test public void testCollateLocalizedForJapanese() throws Exception { final String testName = "DatabaseGeneralTest#testCollateLocalizedForJapanese()"; final Locale[] localeArray = Locale.getAvailableLocales(); @@ -933,7 +943,7 @@ public void testCollateLocalizedForJapanese() throws Exception { } } - @SmallTest + @Test public void testSetMaxCacheSize() { mDatabase.execSQL("CREATE TABLE test (i int, j int);"); mDatabase.execSQL("insert into test values(1,1);"); @@ -950,6 +960,7 @@ public void testSetMaxCacheSize() { } @LargeTest + @Test public void testDefaultDatabaseErrorHandler() { DefaultDatabaseErrorHandler errorHandler = new DefaultDatabaseErrorHandler(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/DatabaseStatementTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/DatabaseStatementTest.java index 2358965..29e465b 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/DatabaseStatementTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/DatabaseStatementTest.java @@ -16,16 +16,26 @@ package net.zetetic.database.sqlcipher_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDoneException; -import android.test.AndroidTestCase; -import android.test.PerformanceTestCase; -import android.test.suitebuilder.annotation.MediumTest; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteStatement; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; /* @@ -34,7 +44,8 @@ * Modifications: * - use Context to create and delete the DB to avoid hard-coded paths */ -public class DatabaseStatementTest extends AndroidTestCase implements PerformanceTestCase { +@RunWith(AndroidJUnit4.class) +public class DatabaseStatementTest { private static final String sString1 = "this is a test"; private static final String sString2 = "and yet another test"; @@ -45,11 +56,10 @@ public class DatabaseStatementTest extends AndroidTestCase implements Performanc private static final int CURRENT_DATABASE_VERSION = 42; private SQLiteDatabase mDatabase; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); - File f = mContext.getDatabasePath(DATABASE_NAME); + File f = ApplicationProvider.getApplicationContext().getDatabasePath(DATABASE_NAME); f.mkdirs(); if (f.exists()) { f.delete(); } mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); @@ -57,22 +67,16 @@ protected void setUp() throws Exception { mDatabase.setVersion(CURRENT_DATABASE_VERSION); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); - getContext().deleteDatabase(DATABASE_NAME); - super.tearDown(); + ApplicationProvider.getApplicationContext().deleteDatabase(DATABASE_NAME); } public boolean isPerformanceOnly() { return false; } - // These test can only be run once. - public int startPerformance(Intermediates intermediates) { - return 1; - } - private void populateDefaultTable() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); @@ -81,7 +85,8 @@ private void populateDefaultTable() { mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString3 + "');"); } - @MediumTest + @Test + @SuppressWarnings("deprecation") public void testExecuteStatement() throws Exception { populateDefaultTable(); SQLiteStatement statement = mDatabase.compileStatement("DELETE FROM test"); @@ -93,7 +98,7 @@ public void testExecuteStatement() throws Exception { statement.close(); } - @MediumTest + @Test public void testSimpleQuery() throws Exception { mDatabase.execSQL("CREATE TABLE test (num INTEGER NOT NULL, str TEXT NOT NULL);"); mDatabase.execSQL("INSERT INTO test VALUES (1234, 'hello');"); @@ -130,7 +135,7 @@ public void testSimpleQuery() throws Exception { statement2.close(); } - @MediumTest + @Test public void testStatementLongBinding() throws Exception { mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test (num) VALUES (?)"); @@ -152,7 +157,7 @@ public void testStatementLongBinding() throws Exception { c.close(); } - @MediumTest + @Test public void testStatementStringBinding() throws Exception { mDatabase.execSQL("CREATE TABLE test (num TEXT);"); SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test (num) VALUES (?)"); @@ -174,7 +179,7 @@ public void testStatementStringBinding() throws Exception { c.close(); } - @MediumTest + @Test public void testStatementClearBindings() throws Exception { mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test (num) VALUES (?)"); @@ -196,7 +201,7 @@ public void testStatementClearBindings() throws Exception { c.close(); } - @MediumTest + @Test public void testSimpleStringBinding() throws Exception { mDatabase.execSQL("CREATE TABLE test (num TEXT, value TEXT);"); String statement = "INSERT INTO test (num, value) VALUES (?,?)"; @@ -220,7 +225,7 @@ public void testSimpleStringBinding() throws Exception { c.close(); } - @MediumTest + @Test public void testStatementMultipleBindings() throws Exception { mDatabase.execSQL("CREATE TABLE test (num INTEGER, str TEXT);"); SQLiteStatement statement = @@ -283,7 +288,7 @@ public void run() { } } - @MediumTest + @Test public void testStatementMultiThreaded() throws Exception { mDatabase.execSQL("CREATE TABLE test (num INTEGER, str TEXT);"); SQLiteStatement statement = @@ -298,7 +303,7 @@ public void testStatementMultiThreaded() throws Exception { } } - @MediumTest + @Test public void testStatementConstraint() throws Exception { mDatabase.execSQL("CREATE TABLE test (num INTEGER NOT NULL);"); SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test (num) VALUES (?)"); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteAbortExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteAbortExceptionTest.java index b92798e..51c1873 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteAbortExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteAbortExceptionTest.java @@ -17,9 +17,16 @@ package net.zetetic.database.sqlcipher_cts; import android.database.sqlite.SQLiteAbortException; -import android.test.AndroidTestCase; -public class SQLiteAbortExceptionTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SQLiteAbortExceptionTest { + + @Test public void testConstructor() { new SQLiteAbortException(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteClosableTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteClosableTest.java index b63795b..e1d5e14 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteClosableTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteClosableTest.java @@ -16,10 +16,20 @@ package net.zetetic.database.sqlcipher_cts; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import net.zetetic.database.sqlcipher.SQLiteClosable; -import android.test.AndroidTestCase; -public class SQLiteClosableTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SuppressWarnings("deprecation") +public class SQLiteClosableTest { private class MockSQLiteClosable extends SQLiteClosable { private boolean mOnAllReferencesReleasedCalled = false; private boolean mOnAllReferencesReleasedFromContainerCalled = false; @@ -42,6 +52,7 @@ public boolean isOnAllReferencesReleasedFromContainerCalled() { } } + @Test public void testAcquireReference() { MockSQLiteClosable closable = new MockSQLiteClosable(); @@ -60,6 +71,8 @@ public void testAcquireReference() { } } + @Test + @SuppressWarnings("deprecation") public void testReleaseReferenceFromContainer() { MockSQLiteClosable closable = new MockSQLiteClosable(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteConstraintExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteConstraintExceptionTest.java index 81e6300..261f865 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteConstraintExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteConstraintExceptionTest.java @@ -17,9 +17,16 @@ package net.zetetic.database.sqlcipher_cts; import android.database.sqlite.SQLiteConstraintException; -import android.test.AndroidTestCase; -public class SQLiteConstraintExceptionTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SQLiteConstraintExceptionTest { + + @Test public void testConstructor() { new SQLiteConstraintException(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java index 5601a5f..1fc54cf 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteCursorTest.java @@ -17,10 +17,19 @@ package net.zetetic.database.sqlcipher_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; import android.database.Cursor; import android.database.DataSetObserver; import android.database.StaleDataException; -import android.test.AndroidTestCase; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.AbstractCursor; import net.zetetic.database.CursorWindow; @@ -28,13 +37,19 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDirectCursorDriver; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; import java.util.Arrays; /** * Test {@link AbstractCursor}. */ -public class SQLiteCursorTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class SQLiteCursorTest { private SQLiteDatabase mDatabase; private static final String[] COLUMNS = new String[] { "_id", "number_1", "number_2" }; private static final String TABLE_NAME = "test"; @@ -43,11 +58,12 @@ public class SQLiteCursorTest extends AndroidTestCase { private static final int TEST_COUNT = 10; private static final String TEST_SQL = "SELECT * FROM test ORDER BY number_1"; private static final String DATABASE_FILE = "database_test.db"; + private Context mContext; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); + mContext = ApplicationProvider.getApplicationContext(); File f = mContext.getDatabasePath(DATABASE_FILE); f.mkdirs(); if (f.exists()) { f.delete(); } @@ -56,13 +72,14 @@ protected void setUp() throws Exception { addValuesIntoTable(TABLE_NAME, DEFAULT_TABLE_VALUE_BEGINS, TEST_COUNT); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); - getContext().deleteDatabase(DATABASE_FILE); - super.tearDown(); + mContext.deleteDatabase(DATABASE_FILE); } + @Test + @SuppressWarnings("deprecation") public void testConstructor() { SQLiteDirectCursorDriver cursorDriver = new SQLiteDirectCursorDriver(mDatabase, TEST_SQL, TABLE_NAME, null); @@ -77,6 +94,7 @@ public void testConstructor() { assertNotNull(cursor); } + @Test public void testClose() { SQLiteCursor cursor = getCursor(); assertTrue(cursor.moveToFirst()); @@ -92,6 +110,7 @@ public void testClose() { assertTrue(cursor.isClosed()); } + @Test public void testRegisterDataSetObserver() { SQLiteCursor cursor = getCursor(); MockCursorWindow cursorWindow = new MockCursorWindow(); @@ -143,6 +162,7 @@ public void testRegisterDataSetObserver() { assertFalse(observer.hasInvalidated()); } + @Test public void testRequery() { final String DELETE = "DELETE FROM " + TABLE_NAME + " WHERE number_1 ="; final String DELETE_1 = DELETE + "1;"; @@ -166,6 +186,8 @@ public void testRequery() { assertTrue(observer.hasChanged()); } + @Test + @SuppressWarnings("deprecation") public void testRequery2() { mDatabase.disableWriteAheadLogging(); mDatabase.execSQL("create table testRequery2 (i int);"); @@ -192,6 +214,7 @@ public void testRequery2() { assertFalse(c.requery()); } + @Test public void testGetColumnIndex() { SQLiteCursor cursor = getCursor(); @@ -202,6 +225,7 @@ public void testGetColumnIndex() { assertTrue(Arrays.equals(COLUMNS, cursor.getColumnNames())); } + @Test public void testSetSelectionArguments() { final String SELECTION = "_id > ?"; int TEST_ARG1 = 2; @@ -214,6 +238,7 @@ public void testSetSelectionArguments() { assertEquals(TEST_COUNT - TEST_ARG2, cursor.getCount()); } + @Test public void testOnMove() { // Do not test this API. It is callback which: // 1. The callback mechanism has been tested in super class diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseCorruptExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseCorruptExceptionTest.java index f79bff8..378e08f 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseCorruptExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseCorruptExceptionTest.java @@ -17,9 +17,16 @@ package net.zetetic.database.sqlcipher_cts; import android.database.sqlite.SQLiteDatabaseCorruptException; -import android.test.AndroidTestCase; -public class SQLiteDatabaseCorruptExceptionTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SQLiteDatabaseCorruptExceptionTest { + + @Test public void testConstructor() { new SQLiteDatabaseCorruptException(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java index a77d6d9..cccd81f 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDatabaseTest.java @@ -16,11 +16,19 @@ package net.zetetic.database.sqlcipher_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.ContentValues; import android.database.Cursor; import android.database.SQLException; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.LargeTest; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.DatabaseUtils; import net.zetetic.database.sqlcipher.SQLiteCursor; @@ -31,11 +39,17 @@ import net.zetetic.database.sqlcipher.SQLiteStatement; import net.zetetic.database.sqlcipher.SQLiteTransactionListener; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; import java.io.IOException; import java.util.concurrent.Semaphore; -public class SQLiteDatabaseTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class SQLiteDatabaseTest { private SQLiteDatabase mDatabase; private File mDatabaseFile; private String mDatabaseFilePath; @@ -58,14 +72,13 @@ public class SQLiteDatabaseTest extends AndroidTestCase { "address" // 3 }; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); - getContext().deleteDatabase(DATABASE_FILE_NAME); - mDatabaseFilePath = getContext().getDatabasePath(DATABASE_FILE_NAME).getPath(); - mDatabaseFile = getContext().getDatabasePath(DATABASE_FILE_NAME); + ApplicationProvider.getApplicationContext().deleteDatabase(DATABASE_FILE_NAME); + mDatabaseFilePath = ApplicationProvider.getApplicationContext().getDatabasePath(DATABASE_FILE_NAME).getPath(); + mDatabaseFile = ApplicationProvider.getApplicationContext().getDatabasePath(DATABASE_FILE_NAME); mDatabaseDir = mDatabaseFile.getParent(); mDatabaseFile.getParentFile().mkdirs(); // directory may not exist mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile, null); @@ -76,13 +89,13 @@ protected void setUp() throws Exception { mTransactionListenerOnRollbackCalled = false; } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); mDatabaseFile.delete(); - super.tearDown(); } + @Test public void testOpenDatabase() { CursorFactory factory = new CursorFactory() { public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, @@ -115,6 +128,7 @@ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, db.close(); } + @Test public void testDeleteDatabase() throws IOException { File dbFile = new File(mDatabaseDir, "database_test12345678.db"); File journalFile = new File(dbFile.getPath() + "-journal"); @@ -149,13 +163,16 @@ public void testDeleteDatabase() throws IOException { assertFalse(deletedAgain); } - private class MockSQLiteCursor extends SQLiteCursor { + @SuppressWarnings("deprecation") + private static class MockSQLiteCursor extends SQLiteCursor { public MockSQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { super(db, driver, editTable, query); } } + @Test + @SuppressWarnings("deprecation") public void testTransaction() { mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); @@ -282,6 +299,7 @@ private void assertThrowsIllegalState(Runnable r) { } } + @Test public void testAccessMaximumSize() { long curMaximumSize = mDatabase.getMaximumSize(); @@ -290,6 +308,7 @@ public void testAccessMaximumSize() { assertEquals(curMaximumSize, mDatabase.getMaximumSize()); } + @Test public void testAccessPageSize() { File databaseFile = new File(mDatabaseDir, "database.db"); if (databaseFile.exists()) { @@ -313,6 +332,8 @@ public void testAccessPageSize() { } } + @Test + @SuppressWarnings("deprecation") public void testCompileStatement() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + "name TEXT, age INTEGER, address TEXT);"); @@ -354,6 +375,7 @@ public void testCompileStatement() { cursor.close(); } + @Test public void testDelete() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + "name TEXT, age INTEGER, address TEXT);"); @@ -408,6 +430,7 @@ public void testDelete() { cursor.close(); } + @Test public void testExecSQL() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + "name TEXT, age INTEGER, address TEXT);"); @@ -498,6 +521,7 @@ public void testExecSQL() { cursor.close();; } + @Test public void testFindEditTable() { String tables = "table1 table2 table3"; assertEquals("table1", SQLiteDatabase.findEditTable(tables)); @@ -515,10 +539,12 @@ public void testFindEditTable() { } } + @Test public void testGetPath() { assertEquals(mDatabaseFilePath, mDatabase.getPath()); } + @Test public void testAccessVersion() { mDatabase.setVersion(1); assertEquals(1, mDatabase.getVersion()); @@ -527,6 +553,7 @@ public void testAccessVersion() { assertEquals(3, mDatabase.getVersion()); } + @Test public void testInsert() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + "name TEXT, age INTEGER, address TEXT);"); @@ -611,6 +638,7 @@ public void testInsert() { } } + @Test public void testIsOpen() { assertTrue(mDatabase.isOpen()); @@ -618,6 +646,7 @@ public void testIsOpen() { assertFalse(mDatabase.isOpen()); } + @Test public void testIsReadOnly() { assertFalse(mDatabase.isReadOnly()); @@ -633,10 +662,13 @@ public void testIsReadOnly() { } } + @Test public void testReleaseMemory() { SQLiteDatabase.releaseMemory(); } + @Test + @SuppressWarnings("deprecation") public void testSetLockingEnabled() { mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); @@ -650,6 +682,7 @@ public void testSetLockingEnabled() { mDatabase.endTransaction(); } + @Test @SuppressWarnings("deprecation") public void testYieldIfContendedWhenNotContended() { assertFalse(mDatabase.yieldIfContended()); @@ -678,6 +711,7 @@ public void testYieldIfContendedWhenNotContended() { mDatabase.endTransaction(); } + @Test @SuppressWarnings("deprecation") public void testYieldIfContendedWhenContended() throws Exception { mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); @@ -730,6 +764,7 @@ public void run() { t.join(); } + @Test public void testQuery() { mDatabase.execSQL("CREATE TABLE employee (_id INTEGER PRIMARY KEY, " + "name TEXT, month INTEGER, salary INTEGER);"); @@ -839,6 +874,7 @@ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, cursor.close(); } + @Test public void testReplace() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + "name TEXT, age INTEGER, address TEXT);"); @@ -904,6 +940,7 @@ public void testReplace() { } } + @Test public void testUpdate() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); @@ -924,6 +961,7 @@ public void testUpdate() { cursor.close(); } + @Test public void testNeedUpgrade() { mDatabase.setVersion(0); assertTrue(mDatabase.needUpgrade(1)); @@ -931,6 +969,7 @@ public void testNeedUpgrade() { assertFalse(mDatabase.needUpgrade(1)); } + @Test public void testSetLocale() { // final String[] STRINGS = { // "c\u00f4t\u00e9", @@ -974,12 +1013,14 @@ public void testSetLocale() { // }); } + @Test public void testOnAllReferencesReleased() { assertTrue(mDatabase.isOpen()); mDatabase.releaseReference(); assertFalse(mDatabase.isOpen()); } + @Test public void testTransactionWithSQLiteTransactionListener() { mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); @@ -1011,6 +1052,7 @@ public void testTransactionWithSQLiteTransactionListener() { assertEquals(mTransactionListenerOnRollbackCalled, false); } + @Test public void testRollbackTransactionWithSQLiteTransactionListener() { mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); @@ -1055,6 +1097,7 @@ public void onRollback() { } } + @Test public void testGroupConcat() { mDatabase.execSQL("CREATE TABLE test (i INT, j TEXT);"); @@ -1088,6 +1131,7 @@ public void testGroupConcat() { // should get no exceptions } + @Test public void testSchemaChanges() { mDatabase.execSQL("CREATE TABLE test (i INT, j INT);"); @@ -1155,6 +1199,7 @@ public void testSchemaChanges() { deleteStatement.close(); } + @Test public void testSchemaChangesNewTable() { mDatabase.execSQL("CREATE TABLE test (i INT, j INT);"); @@ -1219,6 +1264,7 @@ public void testSchemaChangesNewTable() { deleteStatement2.close(); } + @Test public void testSchemaChangesDropTable() { mDatabase.execSQL("CREATE TABLE test (i INT, j INT);"); @@ -1264,7 +1310,8 @@ public void testSchemaChangesDropTable() { *

* @throws InterruptedException */ - @LargeTest + @Test + @SuppressWarnings("deprecation") public void testReaderGetsOldVersionOfDataWhenWriterIsInXact() throws InterruptedException { // redo setup to create WAL enabled database mDatabase.close(); @@ -1338,6 +1385,7 @@ public ReaderQueryingData(int count) { } } + @Test public void testExceptionsFromEnableWriteAheadLogging() { // attach a database // redo setup to create WAL enabled database @@ -1359,6 +1407,7 @@ public void testExceptionsFromEnableWriteAheadLogging() { db.close(); } + @Test public void testEnableThenDisableWriteAheadLogging() { // Enable WAL. assertFalse(mDatabase.isWriteAheadLoggingEnabled()); @@ -1378,6 +1427,7 @@ public void testEnableThenDisableWriteAheadLogging() { assertFalse(mDatabase.isWriteAheadLoggingEnabled()); } + @Test public void testEnableThenDisableWriteAheadLoggingUsingOpenFlag() { new File(mDatabase.getPath()).delete(); mDatabase = SQLiteDatabase.openDatabase(mDatabaseFile.getPath(), null, @@ -1398,11 +1448,12 @@ public void testEnableThenDisableWriteAheadLoggingUsingOpenFlag() { assertFalse(mDatabase.isWriteAheadLoggingEnabled()); } + @Test public void testEnableWriteAheadLoggingFromContextUsingModeFlag() { // Without the MODE_ENABLE_WRITE_AHEAD_LOGGING flag, database opens without WAL. - getContext().deleteDatabase(DATABASE_FILE_NAME); + ApplicationProvider.getApplicationContext().deleteDatabase(DATABASE_FILE_NAME); - File f = getContext().getDatabasePath(DATABASE_FILE_NAME); + File f = ApplicationProvider.getApplicationContext().getDatabasePath(DATABASE_FILE_NAME); mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); assertFalse(mDatabase.isWriteAheadLoggingEnabled()); mDatabase.close(); @@ -1415,6 +1466,7 @@ public void testEnableWriteAheadLoggingFromContextUsingModeFlag() { // mDatabase.close(); } + @Test public void testEnableWriteAheadLoggingShouldThrowIfTransactionInProgress() { assertFalse(mDatabase.isWriteAheadLoggingEnabled()); String oldJournalMode = DatabaseUtils.stringForQuery( @@ -1436,6 +1488,7 @@ public void testEnableWriteAheadLoggingShouldThrowIfTransactionInProgress() { .equalsIgnoreCase(oldJournalMode)); } + @Test public void testDisableWriteAheadLoggingShouldThrowIfTransactionInProgress() { // Enable WAL. assertFalse(mDatabase.isWriteAheadLoggingEnabled()); @@ -1458,6 +1511,7 @@ public void testDisableWriteAheadLoggingShouldThrowIfTransactionInProgress() { .equalsIgnoreCase("WAL")); } + @Test public void testEnableAndDisableForeignKeys() { // Initially off. assertEquals(0, DatabaseUtils.longForQuery(mDatabase, "PRAGMA foreign_keys", null)); @@ -1486,6 +1540,7 @@ public void testEnableAndDisableForeignKeys() { assertEquals(1, DatabaseUtils.longForQuery(mDatabase, "PRAGMA foreign_keys", null)); } + @Test public void testShouldSupportBindingNullValue(){ String b = ""; mDatabase.execSQL("CREATE TABLE t1(a,b);"); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDiskIOExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDiskIOExceptionTest.java index 9cd9d8d..febd2e3 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDiskIOExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDiskIOExceptionTest.java @@ -17,9 +17,16 @@ package net.zetetic.database.sqlcipher_cts; import android.database.sqlite.SQLiteDiskIOException; -import android.test.AndroidTestCase; -public class SQLiteDiskIOExceptionTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SQLiteDiskIOExceptionTest { + + @Test public void testConstructor() { new SQLiteDiskIOException(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDoneExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDoneExceptionTest.java index 231113f..384903a 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDoneExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteDoneExceptionTest.java @@ -17,9 +17,16 @@ package net.zetetic.database.sqlcipher_cts; import android.database.sqlite.SQLiteDoneException; -import android.test.AndroidTestCase; -public class SQLiteDoneExceptionTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SQLiteDoneExceptionTest { + + @Test public void testConstructor() { new SQLiteDoneException(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteExceptionTest.java index 1d9c14b..92d2c56 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteExceptionTest.java @@ -17,9 +17,16 @@ package net.zetetic.database.sqlcipher_cts; import android.database.sqlite.SQLiteException; -import android.test.AndroidTestCase; -public class SQLiteExceptionTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SQLiteExceptionTest { + + @Test public void testConstructor() { new SQLiteException(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFtsTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFtsTest.java index 3f51469..4708ddd 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFtsTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFtsTest.java @@ -16,19 +16,32 @@ package net.zetetic.database.sqlcipher_cts; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import android.content.ContentValues; +import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.test.AndroidTestCase; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + import net.zetetic.database.sqlcipher.SQLiteDatabase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; /** * Tests to verify FTS3/4 SQLite support. */ -public class SQLiteFtsTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class SQLiteFtsTest { private static final String TEST_TABLE = "cts_fts"; @@ -39,10 +52,11 @@ public class SQLiteFtsTest extends AndroidTestCase { }; private SQLiteDatabase mDatabase; + private Context mContext; - @Override + @Before public void setUp() throws Exception { - super.setUp(); + mContext = ApplicationProvider.getApplicationContext(); System.loadLibrary("sqlcipher"); File f = mContext.getDatabasePath("CTS_FTS"); f.mkdirs(); @@ -50,17 +64,14 @@ public void setUp() throws Exception { mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); } - @Override + @After public void tearDown() throws Exception { - try { - final String path = mDatabase.getPath(); - mDatabase.close(); - SQLiteDatabase.deleteDatabase(new File(path)); - } finally { - super.tearDown(); - } + final String path = mDatabase.getPath(); + mDatabase.close(); + SQLiteDatabase.deleteDatabase(new File(path)); } + @Test public void testFts3Porter() throws Exception { prepareFtsTable(TEST_TABLE, "fts3", "tokenize=porter"); @@ -77,6 +88,7 @@ public void testFts3Porter() throws Exception { } } + @Test public void testFts3Simple() throws Exception { prepareFtsTable(TEST_TABLE, "fts3", "tokenize=simple"); @@ -91,6 +103,7 @@ public void testFts3Simple() throws Exception { } } + @Test public void testFts4Simple() throws Exception { prepareFtsTable(TEST_TABLE, "fts4", "tokenize=simple"); @@ -111,7 +124,7 @@ private void prepareFtsTable(String table, String ftsType, String options) "CREATE VIRTUAL TABLE " + table + " USING " + ftsType + "(content TEXT, " + options + ");"); - final Resources res = getContext().getResources(); + final Resources res = mContext.getResources(); final ContentValues values = new ContentValues(); for (String content : TEST_CONTENT) { values.clear(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFullExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFullExceptionTest.java index a62f3cd..2e62200 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFullExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteFullExceptionTest.java @@ -17,9 +17,16 @@ package net.zetetic.database.sqlcipher_cts; import android.database.sqlite.SQLiteFullException; -import android.test.AndroidTestCase; -public class SQLiteFullExceptionTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SQLiteFullExceptionTest { + + @Test public void testConstructor() { new SQLiteFullException(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteMisuseExceptionTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteMisuseExceptionTest.java index dd3bfa3..3b91cd5 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteMisuseExceptionTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteMisuseExceptionTest.java @@ -17,9 +17,16 @@ package net.zetetic.database.sqlcipher_cts; import android.database.sqlite.SQLiteMisuseException; -import android.test.AndroidTestCase; -public class SQLiteMisuseExceptionTest extends AndroidTestCase { +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SQLiteMisuseExceptionTest { + + @Test public void testConstructor() { new SQLiteMisuseException(); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteOpenHelperTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteOpenHelperTest.java index 822cfec..5adc5c7 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteOpenHelperTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteOpenHelperTest.java @@ -16,9 +16,18 @@ package net.zetetic.database.sqlcipher_cts; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.Context; import android.database.Cursor; -import android.test.AndroidTestCase; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.sqlcipher.SQLiteCursor; import net.zetetic.database.sqlcipher.SQLiteCursorDriver; @@ -27,15 +36,21 @@ import net.zetetic.database.sqlcipher.SQLiteOpenHelper; import net.zetetic.database.sqlcipher.SQLiteQuery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + /** * Test {@link SQLiteOpenHelper}. */ -public class SQLiteOpenHelperTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class SQLiteOpenHelperTest { private static final String TEST_DATABASE_NAME = "database_test.db"; static String DATABASE_PATH; private static final int TEST_VERSION = 1; private static final int TEST_ILLEGAL_VERSION = 0; private MockOpenHelper mOpenHelper; + private Context mContext; private SQLiteDatabase.CursorFactory mFactory = new SQLiteDatabase.CursorFactory() { public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, String editTable, SQLiteQuery query) { @@ -43,14 +58,15 @@ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, } }; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); + mContext = ApplicationProvider.getApplicationContext(); DATABASE_PATH = mContext.getDatabasePath(TEST_DATABASE_NAME).toString(); mOpenHelper = getOpenHelper(); } + @Test public void testConstructor() { new MockOpenHelper(mContext, DATABASE_PATH, mFactory, TEST_VERSION); @@ -65,6 +81,7 @@ public void testConstructor() { new MockOpenHelper(mContext, DATABASE_PATH, null, TEST_VERSION); } + @Test public void testGetDatabase() { SQLiteDatabase database = null; assertFalse(mOpenHelper.hasCalledOnOpen()); @@ -131,6 +148,7 @@ public void resetStatus() { } } + @SuppressWarnings("deprecation") private class MockCursor extends SQLiteCursor { public MockCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteProgramTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteProgramTest.java index 47ddacd..5317b03 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteProgramTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteProgramTest.java @@ -17,25 +17,40 @@ package net.zetetic.database.sqlcipher_cts; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteException; -import android.test.AndroidTestCase; -import android.test.MoreAsserts; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteStatement; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.io.File; -public class SQLiteProgramTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class SQLiteProgramTest { private static final String DATABASE_NAME = "database_test.db"; private SQLiteDatabase mDatabase; + private Context mContext; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { + mContext = ApplicationProvider.getApplicationContext(); System.loadLibrary("sqlcipher"); File f = mContext.getDatabasePath(DATABASE_NAME); f.mkdirs(); @@ -44,14 +59,14 @@ protected void setUp() throws Exception { assertNotNull(mDatabase); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); - getContext().deleteDatabase(DATABASE_NAME); + mContext.deleteDatabase(DATABASE_NAME); - super.tearDown(); } + @Test public void testBind() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + "num1 INTEGER, num2 INTEGER, image BLOB);"); @@ -122,6 +137,7 @@ public void testBind() { statement.close(); } + @Test public void testBindNull() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + "num1 INTEGER, num2 INTEGER, image BLOB);"); @@ -152,6 +168,7 @@ public void testBindNull() { cursor.close(); } + @Test public void testBindBlob() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + "num1 INTEGER, num2 INTEGER, image BLOB);"); @@ -180,7 +197,7 @@ public void testBindBlob() { assertEquals("string2", cursor.getString(COLUMN_TEXT2_INDEX)); assertEquals(100, cursor.getInt(COLUMN_NUM1_INDEX)); byte[] value = cursor.getBlob(COLUMN_IMAGE_INDEX); - MoreAsserts.assertEquals(blob, value); + assertArrayEquals(blob, value); cursor.close(); } } diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryBuilderTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryBuilderTest.java index aef5c5e..99a7ac1 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryBuilderTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteQueryBuilderTest.java @@ -20,7 +20,9 @@ import android.database.Cursor; import android.os.CancellationSignal; import android.os.OperationCanceledException; -import android.test.AndroidTestCase; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import net.zetetic.database.sqlcipher.SQLiteCursor; import net.zetetic.database.sqlcipher.SQLiteCursorDriver; @@ -28,39 +30,51 @@ import net.zetetic.database.sqlcipher.SQLiteQuery; import net.zetetic.database.sqlcipher.SQLiteQueryBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Semaphore; -public class SQLiteQueryBuilderTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class SQLiteQueryBuilderTest { private SQLiteDatabase mDatabase; private final String TEST_TABLE_NAME = "test"; private final String EMPLOYEE_TABLE_NAME = "employee"; private static final String DATABASE_FILE = "database_test.db"; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { System.loadLibrary("sqlcipher"); - File f = mContext.getDatabasePath(DATABASE_FILE); + File f = ApplicationProvider.getApplicationContext().getDatabasePath(DATABASE_FILE); f.mkdirs(); if (f.exists()) { f.delete(); } mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); assertNotNull(mDatabase); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); - getContext().deleteDatabase(DATABASE_FILE); - super.tearDown(); + ApplicationProvider.getApplicationContext().deleteDatabase(DATABASE_FILE); } + @Test public void testConstructor() { new SQLiteQueryBuilder(); } + @Test + @SuppressWarnings("deprecation") public void testSetDistinct() { String expected; SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); @@ -95,38 +109,8 @@ public void testSetDistinct() { assertEquals(expected, sql); } - public void testSetProjectionMap() { - String expected; - Map projectMap = new HashMap(); - projectMap.put("EmployeeName", "name"); - projectMap.put("EmployeeAge", "age"); - projectMap.put("EmployeeAddress", "address"); - SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); - sqliteQueryBuilder.setTables(TEST_TABLE_NAME); - sqliteQueryBuilder.setDistinct(false); - sqliteQueryBuilder.setProjectionMap(projectMap); - String sql = sqliteQueryBuilder.buildQuery(new String[] { "EmployeeName", "EmployeeAge" }, - null, null, null, null, null, null); - expected = "SELECT name, age FROM " + TEST_TABLE_NAME; - assertEquals(expected, sql); - - sql = sqliteQueryBuilder.buildQuery(null, // projectionIn is null - null, null, null, null, null, null); - assertTrue(sql.matches("SELECT (age|name|address), (age|name|address), (age|name|address) " - + "FROM " + TEST_TABLE_NAME)); - assertTrue(sql.contains("age")); - assertTrue(sql.contains("name")); - assertTrue(sql.contains("address")); - - sqliteQueryBuilder.setProjectionMap(null); - sql = sqliteQueryBuilder.buildQuery(new String[] { "name", "address" }, - null, null, null, null, null, null); - assertTrue(sql.matches("SELECT (name|address), (name|address) " - + "FROM " + TEST_TABLE_NAME)); - assertTrue(sql.contains("name")); - assertTrue(sql.contains("address")); - } - + @Test + @SuppressWarnings("deprecation") public void testSetCursorFactory() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + "name TEXT, age INTEGER, address TEXT);"); @@ -154,6 +138,41 @@ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, assertTrue(cursor instanceof MockCursor); } + @Test + @SuppressWarnings("deprecation") + public void testSetProjectionMap() { + String expected; + Map projectMap = new HashMap(); + projectMap.put("EmployeeName", "name"); + projectMap.put("EmployeeAge", "age"); + projectMap.put("EmployeeAddress", "address"); + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables(TEST_TABLE_NAME); + sqliteQueryBuilder.setDistinct(false); + sqliteQueryBuilder.setProjectionMap(projectMap); + String sql = sqliteQueryBuilder.buildQuery(new String[] { "EmployeeName", "EmployeeAge" }, + null, null, null, null, null, null); + expected = "SELECT name, age FROM " + TEST_TABLE_NAME; + assertEquals(expected, sql); + + sql = sqliteQueryBuilder.buildQuery(null, // projectionIn is null + null, null, null, null, null, null); + assertTrue(sql.matches("SELECT (age|name|address), (age|name|address), (age|name|address) " + + "FROM " + TEST_TABLE_NAME)); + assertTrue(sql.contains("age")); + assertTrue(sql.contains("name")); + assertTrue(sql.contains("address")); + + sqliteQueryBuilder.setProjectionMap(null); + sql = sqliteQueryBuilder.buildQuery(new String[] { "name", "address" }, + null, null, null, null, null, null); + assertTrue(sql.matches("SELECT (name|address), (name|address) " + + "FROM " + TEST_TABLE_NAME)); + assertTrue(sql.contains("name")); + assertTrue(sql.contains("address")); + } + + @SuppressWarnings("deprecation") private static class MockCursor extends SQLiteCursor { public MockCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { @@ -161,6 +180,7 @@ public MockCursor(SQLiteDatabase db, SQLiteCursorDriver driver, } } + @Test public void testBuildQueryString() { String expected; final String[] DEFAULT_TEST_PROJECTION = new String [] { "name", "age", "sum(salary)" }; @@ -179,6 +199,8 @@ public void testBuildQueryString() { assertEquals(expected, sql); } + @Test + @SuppressWarnings("deprecation") public void testBuildQuery() { final String[] DEFAULT_TEST_PROJECTION = new String[] { "name", "sum(salary)" }; final String DEFAULT_TEST_WHERE = "age > 25"; @@ -195,6 +217,7 @@ public void testBuildQuery() { assertEquals(expected, sql); } + @Test public void testAppendColumns() { StringBuilder sb = new StringBuilder(); String[] columns = new String[] { "name", "age" }; @@ -204,6 +227,7 @@ public void testAppendColumns() { assertEquals("name, age ", sb.toString()); } + @Test public void testQuery() { createEmployeeTable(); @@ -243,6 +267,8 @@ public void testQuery() { assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX)); } + @Test + @SuppressWarnings("deprecation") public void testUnionQuery() { String expected; String[] innerProjection = new String[] {"name", "age", "location"}; @@ -278,6 +304,7 @@ public void testUnionQuery() { assertEquals(expected, unionQuery); } + @Test public void testCancelableQuery_WhenNotCanceled_ReturnsResultSet() { createEmployeeTable(); @@ -291,6 +318,7 @@ public void testCancelableQuery_WhenNotCanceled_ReturnsResultSet() { assertEquals(3, cursor.getCount()); } + @Test public void testCancelableQuery_WhenCanceledBeforeQuery_ThrowsImmediately() { createEmployeeTable(); @@ -309,6 +337,7 @@ public void testCancelableQuery_WhenCanceledBeforeQuery_ThrowsImmediately() { } } + @Test public void testCancelableQuery_WhenCanceledAfterQuery_ThrowsWhenExecuted() { createEmployeeTable(); @@ -329,6 +358,7 @@ public void testCancelableQuery_WhenCanceledAfterQuery_ThrowsWhenExecuted() { } } + @Test public void testCancelableQuery_WhenCanceledDueToContention_StopsWaitingAndThrows() { createEmployeeTable(); @@ -404,6 +434,7 @@ public void run() { fail("Could not prove that the query actually blocked before cancel() was called."); } + @Test public void testCancelableQuery_WhenCanceledDuringLongRunningQuery_CancelsQueryAndThrows() { // Populate a table with a bunch of integers. mDatabase.execSQL("CREATE TABLE x (v INTEGER);"); diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteStatementTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteStatementTest.java index ffd4441..c9f62a9 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteStatementTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLiteStatementTest.java @@ -17,7 +17,14 @@ package net.zetetic.database.sqlcipher_cts; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; import net.zetetic.database.DatabaseUtils; import net.zetetic.database.sqlcipher.SQLiteDatabase; @@ -26,15 +33,22 @@ import android.database.SQLException; import android.database.sqlite.SQLiteDoneException; import android.os.ParcelFileDescriptor; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.Suppress; -import android.test.AndroidTestCase; -import android.test.MoreAsserts; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import java.io.IOException; import java.io.InputStream; import java.io.File; -public class SQLiteStatementTest extends AndroidTestCase { +@RunWith(AndroidJUnit4.class) +public class SQLiteStatementTest { private static final String STRING1 = "this is a test"; private static final String STRING2 = "another test"; @@ -51,12 +65,13 @@ public class SQLiteStatementTest extends AndroidTestCase { private static final int CURRENT_DATABASE_VERSION = 42; private SQLiteDatabase mDatabase; + private Context mContext; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { + mContext = ApplicationProvider.getApplicationContext(); System.loadLibrary("sqlcipher"); - getContext().deleteDatabase(DATABASE_NAME); + mContext.deleteDatabase(DATABASE_NAME); File f = mContext.getDatabasePath(DATABASE_NAME); f.mkdirs(); if (f.exists()) { f.delete(); } @@ -65,11 +80,10 @@ protected void setUp() throws Exception { mDatabase.setVersion(CURRENT_DATABASE_VERSION); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { mDatabase.close(); - getContext().deleteDatabase(DATABASE_NAME); - super.tearDown(); + mContext.deleteDatabase(DATABASE_NAME); } private void populateDefaultTable() { @@ -86,6 +100,7 @@ private void populateBlobTable() { } } + @Test public void testExecute() { mDatabase.disableWriteAheadLogging(); populateDefaultTable(); @@ -129,6 +144,7 @@ public void testExecute() { } } + @Test public void testExecuteInsert() { populateDefaultTable(); @@ -168,6 +184,7 @@ public void testExecuteInsert() { } } + @Test public void testSimpleQueryForLong() { mDatabase.execSQL("CREATE TABLE test (num INTEGER NOT NULL, str TEXT NOT NULL);"); mDatabase.execSQL("INSERT INTO test VALUES (1234, 'hello');"); @@ -192,6 +209,7 @@ public void testSimpleQueryForLong() { statement.close(); } + @Test public void testSimpleQueryForString() { mDatabase.execSQL("CREATE TABLE test (num INTEGER NOT NULL, str TEXT NOT NULL);"); mDatabase.execSQL("INSERT INTO test VALUES (1234, 'hello');"); @@ -217,16 +235,19 @@ public void testSimpleQueryForString() { } @Suppress + @Test public void testSimpleQueryForBlobFileDescriptorSuccessNormal() throws IOException { doTestSimpleQueryForBlobFileDescriptorSuccess(0); } @Suppress + @Test public void testSimpleQueryForBlobFileDescriptorSuccessEmpty() throws IOException { doTestSimpleQueryForBlobFileDescriptorSuccess(1); } @Suppress + @Test public void testSimpleQueryForBlobFileDescriptorSuccessNull() { populateBlobTable(); @@ -236,22 +257,25 @@ public void testSimpleQueryForBlobFileDescriptorSuccessNull() { } @Suppress + @Test public void testSimpleQueryForBlobFileDescriptorSuccess00() throws IOException { doTestSimpleQueryForBlobFileDescriptorSuccess(3); } @Suppress + @Test public void testSimpleQueryForBlobFileDescriptorSuccessFF() throws IOException { doTestSimpleQueryForBlobFileDescriptorSuccess(4); } @Suppress + @Test public void testSimpleQueryForBlobFileDescriptorSuccessEmbeddedNul() throws IOException { doTestSimpleQueryForBlobFileDescriptorSuccess(5); } @Suppress - private void doTestSimpleQueryForBlobFileDescriptorSuccess(int i) throws IOException { + public void doTestSimpleQueryForBlobFileDescriptorSuccess(int i) throws IOException { populateBlobTable(); String sql = "SELECT data FROM blob_test WHERE _id = " + i; @@ -261,6 +285,7 @@ private void doTestSimpleQueryForBlobFileDescriptorSuccess(int i) throws IOExcep } @Suppress + @Test public void testSimpleQueryForBlobFileDescriptorSuccessParam() throws IOException { populateBlobTable(); @@ -271,6 +296,7 @@ public void testSimpleQueryForBlobFileDescriptorSuccessParam() throws IOExceptio assertFileDescriptorContent(BLOBS[0], fd); } + @Test public void testGetBlobFailureNoParam() throws Exception { populateBlobTable(); @@ -291,6 +317,7 @@ public void testGetBlobFailureNoParam() throws Exception { assertNotNull("Should have thrown SQLiteDoneException", expectedException); } + @Test public void testGetBlobFailureParam() throws Exception { populateBlobTable(); @@ -346,7 +373,7 @@ private static void assertInputStreamContent(byte[] expected, InputStream is) int count = is.read(observed); assertEquals(expected.length, count); assertEquals(-1, is.read()); - MoreAsserts.assertEquals(expected, observed); + assertArrayEquals(expected, observed); } finally { is.close(); } diff --git a/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java b/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java index b953229..b0c7fa7 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java +++ b/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java @@ -96,6 +96,7 @@ public int getColumnCount() { } @Override + @SuppressWarnings("deprecation") public void deactivate() { onDeactivateOrClose(); } @@ -110,6 +111,7 @@ protected void onDeactivateOrClose() { } @Override + @SuppressWarnings("deprecation") public boolean requery() { if (mSelfObserver != null && !mSelfObserverRegistered) { mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteGlobal.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteGlobal.java index 60e1cf9..91e4503 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteGlobal.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteGlobal.java @@ -62,6 +62,7 @@ public static int releaseMemory() { /** * Gets the default page size to use when creating a database. */ + @SuppressWarnings("deprecation") public static int getDefaultPageSize() { synchronized (sLock) { if (sDefaultPageSize == 0) { From 1bea27c257bcdf4331907d93d4d0cd2cf93985c9 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Tue, 25 Mar 2025 09:39:07 -0500 Subject: [PATCH 21/25] Adjustments for publishing to Maven Central --- sqlcipher/build.gradle | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/sqlcipher/build.gradle b/sqlcipher/build.gradle index 4c65497..9732cc5 100644 --- a/sqlcipher/build.gradle +++ b/sqlcipher/build.gradle @@ -65,11 +65,6 @@ android { withSourcesJar() withJavadocJar() } - - singleVariant('debug') { - withSourcesJar() - withJavadocJar() - } } } @@ -142,7 +137,6 @@ afterEvaluate { artifactId = "sqlcipher-android" version = "${rootProject.ext.mavenVersionName}" from components.release - artifact androidSourcesJar pom { name = "sqlcipher-android" packaging = "aar" @@ -189,13 +183,4 @@ afterEvaluate { required { isReleaseBuild() && gradle.taskGraph.hasTask("publish") } sign publishing.publications.mavenJava } - - artifacts { - archives androidSourcesJar - } -} - -task androidSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs } From 007a3f7cd9acff09708511107172c8b6cc4d7148 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Wed, 26 Mar 2025 14:18:33 -0500 Subject: [PATCH 22/25] Disable ALOGD/ALOGI when LOG_NDEBUG/NDEBUG defined --- sqlcipher/src/main/jni/sqlcipher/ALog-priv.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sqlcipher/src/main/jni/sqlcipher/ALog-priv.h b/sqlcipher/src/main/jni/sqlcipher/ALog-priv.h index 30d7e78..18d0aac 100644 --- a/sqlcipher/src/main/jni/sqlcipher/ALog-priv.h +++ b/sqlcipher/src/main/jni/sqlcipher/ALog-priv.h @@ -14,7 +14,7 @@ * limitations under the License. */ /* -** Modified to support SQLite extensions by the SQLite developers: +** Modified to support SQLite extensions by the SQLite developers: ** sqlite-dev@sqlite.org. */ @@ -52,12 +52,20 @@ #endif #ifndef ALOGD +#if LOG_NDEBUG +#define ALOGD(...) ((void)0) +#else #define ALOGD(...) ((void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) #endif +#endif #ifndef ALOGI +#if LOG_NDEBUG +#define ALOGI(...) ((void)0) +#else #define ALOGI(...) ((void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) #endif +#endif #ifndef ALOGW #define ALOGW(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) From f3475e86aeecd29f13a3ba7bca971d7753b8bfa4 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Fri, 28 Mar 2025 14:48:15 -0500 Subject: [PATCH 23/25] Fix crash related to invalid RowSlot or RowSlotChunk pointers Details: Fix two issues where a RowSlot* or RowSlotChunk* could point to invalid addresses following a CursorWindow resize which moves the entire allocation block, invalidating the previous address. These scenarios could occur when a query required expanding the default CursorWindow size and the allocation occured on an edge boundary. This adjustment also allows us to remove the CURSOR_SIZE_EXTRA which caused the test `testManyRowsLong` to identify the RowSlotChunk issue. The test `shouldNotCauseRowSlotAllocationCrash` verified the fix for the RowSlot error. --- .../AndroidSQLCipherTestCase.java | 205 ++++++++++++++++++ .../sqlcipher_cts/SQLCipherDatabaseTest.java | 119 ++++++++++ .../src/main/jni/sqlcipher/CursorWindow.cpp | 16 +- .../src/main/jni/sqlcipher/CursorWindow.h | 1 - 4 files changed, 335 insertions(+), 6 deletions(-) diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/AndroidSQLCipherTestCase.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/AndroidSQLCipherTestCase.java index 776d92a..07bc2ef 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/AndroidSQLCipherTestCase.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/AndroidSQLCipherTestCase.java @@ -1,6 +1,7 @@ package net.zetetic.database.sqlcipher_cts; import android.content.Context; +import android.icu.text.NumberFormat; import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -19,6 +20,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Locale; @RunWith(AndroidJUnit4.class) @@ -105,4 +109,205 @@ protected void log(String message, Object... args) { protected void loge(Exception ex, String message, Object... args) { Log.e(TAG, String.format(Locale.getDefault(), message, args), ex); } + + public interface RowColumnValueBuilder { + Object buildRowColumnValue(String[] columns, int row, int column); + } + + protected void buildDatabase( + SQLiteDatabase database, + int rows, + int columns, + RowColumnValueBuilder builder) { + var columnNames = new ArrayList(); + Log.i(TAG, String.format("Building database with %s rows, %d columns", + NumberFormat.getInstance().format(rows), columns)); + var createTemplate = "CREATE TABLE t1(%s);"; + var insertTemplate = "INSERT INTO t1 VALUES(%s);"; + var createBuilder = new StringBuilder(); + var insertBuilder = new StringBuilder(); + for (int column = 0; column < columns; column++) { + var columnName = generateColumnName(columnNames, column); + createBuilder.append(String.format("%s BLOB%s", + columnName, + column != columns - 1 ? "," : "")); + insertBuilder.append(String.format("?%s", column != columns - 1 ? "," : "")); + } + var create = String.format(createTemplate, createBuilder.toString()); + var insert = String.format(insertTemplate, insertBuilder.toString()); + database.execSQL("DROP TABLE IF EXISTS t1;"); + database.execSQL(create); + database.execSQL("BEGIN;"); + var names = columnNames.toArray(new String[0]); + for (int row = 0; row < rows; row++) { + var insertArgs = new Object[columns]; + for (var column = 0; column < columns; column++) { + insertArgs[column] = builder.buildRowColumnValue(names, row, column); + } + database.execSQL(insert, insertArgs); + } + database.execSQL("COMMIT;"); + Log.i(TAG, String.format("Database built with %d columns, %d rows", columns, rows)); + } + + protected Integer[] generateRandomNumbers( + int max, + int times){ + var random = new SecureRandom(); + var numbers = new ArrayList<>(); + for(var index = 0; index < times; index++){ + boolean alreadyExists; + do { + var value = random.nextInt(max); + alreadyExists = numbers.contains(value); + if(!alreadyExists){ + numbers.add(value); + } + } while(alreadyExists); + } + return numbers.toArray(new Integer[0]); + } + + protected List ReservedWords = Arrays.asList( + "ABORT", + "ACTION", + "ADD", + "AFTER", + "ALL", + "ALTER", + "ANALYZE", + "AND", + "AS", + "ASC", + "ATTACH", + "AUTOINCREMENT", + "BEFORE", + "BEGIN", + "BETWEEN", + "BY", + "CASCADE", + "CASE", + "CAST", + "CHECK", + "COLLATE", + "COLUMN", + "COMMIT", + "CONFLICT", + "CONSTRAINT", + "CREATE", + "CROSS", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "DATABASE", + "DEFAULT", + "DEFERRABLE", + "DEFERRED", + "DELETE", + "DESC", + "DETACH", + "DISTINCT", + "DROP", + "EACH", + "ELSE", + "END", + "ESCAPE", + "EXCEPT", + "EXCLUSIVE", + "EXISTS", + "EXPLAIN", + "FAIL", + "FOR", + "FOREIGN", + "FROM", + "FULL", + "GLOB", + "GROUP", + "HAVING", + "IF", + "IGNORE", + "IMMEDIATE", + "IN", + "INDEX", + "INDEXED", + "INITIALLY", + "INNER", + "INSERT", + "INSTEAD", + "INTERSECT", + "INTO", + "IS", + "ISNULL", + "JOIN", + "KEY", + "LEFT", + "LIKE", + "LIMIT", + "MATCH", + "NATURAL", + "NO", + "NOT", + "NOTNULL", + "NULL", + "OF", + "OFFSET", + "ON", + "OR", + "ORDER", + "OUTER", + "PLAN", + "PRAGMA", + "PRIMARY", + "QUERY", + "RAISE", + "RECURSIVE", + "REFERENCES", + "REGEXP", + "REINDEX", + "RELEASE", + "RENAME", + "REPLACE", + "RESTRICT", + "RIGHT", + "ROLLBACK", + "ROW", + "SAVEPOINT", + "SELECT", + "SET", + "TABLE", + "TEMP", + "TEMPORARY", + "THEN", + "TO", + "TRANSACTION", + "TRIGGER", + "UNION", + "UNIQUE", + "UPDATE", + "USING", + "VACUUM", + "VALUES", + "VIEW", + "VIRTUAL", + "WHEN", + "WHERE", + "WITH", + "WITHOUT"); + + private String generateColumnName( + List columnNames, + int columnIndex){ + var random = new SecureRandom(); + var labels = "abcdefghijklmnopqrstuvwxyz"; + var element = columnIndex < labels.length() + ? String.valueOf(labels.charAt(columnIndex)) + : String.valueOf(labels.charAt(random.nextInt(labels.length() - 1))); + while(columnNames.contains(element) || ReservedWords.contains(element.toUpperCase())){ + element += labels.charAt(random.nextInt(labels.length() - 1)); + } + columnNames.add(element); + Log.i(TAG, String.format("Generated column name:%s for index:%d", element, columnIndex)); + return element; + } + } diff --git a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java index 68f8a1d..0e59dfb 100644 --- a/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java +++ b/sqlcipher/src/androidTest/java/net/zetetic/database/sqlcipher_cts/SQLCipherDatabaseTest.java @@ -18,9 +18,14 @@ import org.junit.Test; import java.io.File; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.Arrays; import java.util.HashMap; +import java.util.Random; import java.util.UUID; public class SQLCipherDatabaseTest extends AndroidSQLCipherTestCase { @@ -571,4 +576,118 @@ public void shouldSupportDeleteWithNullWhereArgs(){ } assertThat(rowsFound, is(0L)); } + + + // This test recreated a scenario where the CursorWindow::allocRow + // would alloc a RowSlot*, then the alloc call to allocate the + // fieldDirOffset (based on fieldDirSize) would cause mData + // to move (when there was just enough space in mData for a RowSlot*, + // but not enough for the corresponding FieldSlot*), invalidating the + // previous rowSlot address. This has been addressed by reassigning the + // rowSlot pointer after alloc, prior to binding the fieldDirOffset + // and should not fail now. + /** @noinspection StatementWithEmptyBody*/ + @Test + public void shouldNotCauseRowSlotAllocationCrash(){ + SQLiteCursor.resetCursorWindowSize(); + database.execSQL("create table t1(a INTEGER, b INTEGER, c TEXT, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, "+ + "aa, bb, cc, dd, ee, ff, gg, hh, ii, jj, kk, ll, mm, nn, oo, pp, qq, rr, ss, tt, uu, vv);"); + database.beginTransaction(); + Random random = new Random(); + for(int i = 0; i < 20; i++) { + int size = 1024*3 + 450; + database.execSQL("insert into t1(a, b, c) values(?, ?, randomblob(?));", new Object[]{i, size, size}); + } + database.setTransactionSuccessful(); + var value = false; + var cursor = database.rawQuery("select * from t1;"); + if(cursor != null){ + while(cursor.moveToNext()){} + value = true; + } + assertThat(value, is(true)); + } + + @Test + public void shouldBuildLargeDatabaseWithCustomCursorSizeAndNavigateValuesWithDigest() throws UnsupportedEncodingException, NoSuchAlgorithmException { + int rowCount = 1000; + int windowAllocationSize = 1024 * 1024 / 20; + buildDatabase(database, rowCount, 30, (columns, row, column) -> { + try { + var digest = MessageDigest.getInstance("SHA-1"); + var columnName = columns[column]; + var value = String.format("%s%d", columnName, row); + return digest.digest(value.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + Log.e(TAG, e.toString()); + return null; + } + }); + + var randomRows = generateRandomNumbers(rowCount, rowCount); + SQLiteCursor.setCursorWindowSize(windowAllocationSize); + var cursor = database.rawQuery("SELECT * FROM t1;", null); + var digest = MessageDigest.getInstance("SHA-1"); + int row = 0; + Log.i(TAG, "Walking cursor forward"); + while(cursor.moveToNext()){ + var compare = compareDigestForAllColumns(cursor, digest, row); + assertThat(compare, is(true)); + row++; + } + Log.i(TAG, "Walking cursor backward"); + while(cursor.moveToPrevious()){ + row--; + var compare = compareDigestForAllColumns(cursor, digest, row); + assertThat(compare, is(true)); + } + Log.i(TAG, "Walking cursor randomly"); + for(int randomRow : randomRows){ + cursor.moveToPosition(randomRow); + var compare = compareDigestForAllColumns(cursor, digest, randomRow); + assertThat(compare, is(true)); + } + } + + @Test + public void shouldCheckAllTypesFromCursor(){ + database.execSQL("drop table if exists t1;"); + database.execSQL("create table t1(a text, b integer, c text, d real, e blob)"); + byte[] data = new byte[10]; + new SecureRandom().nextBytes(data); + database.execSQL("insert into t1(a, b, c, d, e) values(?, ?, ?, ?, ?)", new Object[]{"test1", 100, null, 3.25, data}); + Cursor results = database.rawQuery("select * from t1", new String[]{}); + results.moveToFirst(); + int type_a = results.getType(0); + int type_b = results.getType(1); + int type_c = results.getType(2); + int type_d = results.getType(3); + int type_e = results.getType(4); + results.close(); + assertThat(type_a, is(Cursor.FIELD_TYPE_STRING)); + assertThat(type_b, is(Cursor.FIELD_TYPE_INTEGER)); + assertThat(type_c, is(Cursor.FIELD_TYPE_NULL)); + assertThat(type_d, is(Cursor.FIELD_TYPE_FLOAT)); + assertThat(type_e, is(Cursor.FIELD_TYPE_BLOB)); + } + + private boolean compareDigestForAllColumns( + Cursor cursor, + MessageDigest digest, + int row) throws UnsupportedEncodingException { + var columnCount = cursor.getColumnCount(); + for(var column = 0; column < columnCount; column++){ + Log.i(TAG, String.format("Comparing SHA-1 digest for row:%d", row)); + var columnName = cursor.getColumnName(column); + var actual = cursor.getBlob(column); + var value = String.format("%s%d", columnName, row); + var expected = digest.digest(value.getBytes(StandardCharsets.UTF_8)); + if(!Arrays.equals(actual, expected)){ + Log.e(TAG, String.format("SHA-1 digest mismatch for row:%d column:%d", row, column)); + return false; + } + } + return true; + } + } diff --git a/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp index 2ead43f..65c7c84 100644 --- a/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp +++ b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp @@ -40,7 +40,7 @@ namespace android { status_t CursorWindow::create(const char* name, size_t size, CursorWindow** outWindow) { status_t result; - size_t requestedSize = size + CURSOR_SIZE_EXTRA; + size_t requestedSize = size; void* data = malloc(requestedSize); if (!data) { return NO_MEMORY; @@ -105,8 +105,10 @@ namespace android { "from allocRowSlot %d", mHeader->numRows); return NO_MEMORY; } + auto* fieldDir = static_cast(offsetToPtr(fieldDirOffset)); memset(fieldDir, 0, fieldDirSize); + rowSlot = getRowSlot(mHeader->numRows - 1); rowSlot->offset = fieldDirOffset; return OK; } @@ -122,10 +124,10 @@ namespace android { } status_t CursorWindow::maybeInflate() { - size_t initialSize = 16 * 1024 + CURSOR_SIZE_EXTRA; + size_t initialSize = 16 * 1024; size_t newSize = mSize <= initialSize - ? 2048 * 1024 + CURSOR_SIZE_EXTRA - : mSize * 2 + CURSOR_SIZE_EXTRA; + ? 2048 * 1024 + : mSize * 2; uint32_t freeOffset = mHeader->freeOffset; ALOGW("Request to resize CursorWindow allocation: current window size %zu bytes, " "free space %zu bytes, new window size %zu bytes", @@ -180,15 +182,19 @@ namespace android { CursorWindow::RowSlot* CursorWindow::allocRowSlot() { uint32_t chunkPos = mHeader->numRows; + uint32_t chunkOffset = mHeader->firstChunkOffset; auto* chunk = static_cast( offsetToPtr(mHeader->firstChunkOffset)); while (chunkPos > ROW_SLOT_CHUNK_NUM_ROWS) { + chunkOffset = chunk->nextChunkOffset; chunk = static_cast(offsetToPtr(chunk->nextChunkOffset)); chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS; } if (chunkPos == ROW_SLOT_CHUNK_NUM_ROWS) { if (!chunk->nextChunkOffset) { - chunk->nextChunkOffset = alloc(sizeof(RowSlotChunk), true /*aligned*/); + uint32_t nextChunkOffset = alloc(sizeof(RowSlotChunk), true /*aligned*/); + chunk = static_cast(offsetToPtr(chunkOffset)); + chunk->nextChunkOffset = nextChunkOffset; if (!chunk->nextChunkOffset) { return nullptr; } diff --git a/sqlcipher/src/main/jni/sqlcipher/CursorWindow.h b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.h index 769ed79..6882a3a 100644 --- a/sqlcipher/src/main/jni/sqlcipher/CursorWindow.h +++ b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.h @@ -123,7 +123,6 @@ namespace android { private: static const size_t ROW_SLOT_CHUNK_NUM_ROWS = 100; - static const size_t CURSOR_SIZE_EXTRA = 512; struct Header { // Offset of the lowest unused byte in the window. From 0c5204a87ad50d24007343441e3711504d947cf5 Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Fri, 11 Apr 2025 13:45:33 -0500 Subject: [PATCH 24/25] Make SQLiteNotADatabaseException public --- .../zetetic/database/sqlcipher/SQLiteNotADatabaseException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteNotADatabaseException.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteNotADatabaseException.java index 82f0456..7ab7a17 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteNotADatabaseException.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteNotADatabaseException.java @@ -7,7 +7,7 @@ * * SQLITE_NOTADB */ -class SQLiteNotADatabaseException extends SQLiteException { +public class SQLiteNotADatabaseException extends SQLiteException { public SQLiteNotADatabaseException() { super(); } From ffe6c588b06ab042d60264f1f0da79a7ac56db0f Mon Sep 17 00:00:00 2001 From: Nick Parker Date: Thu, 17 Apr 2025 12:14:08 -0500 Subject: [PATCH 25/25] Replace android.util.Log with custom logger implementation, updated documentation. --- README.md | 28 +++++++ README.md.template | 28 +++++++ .../net/zetetic/database/AbstractCursor.java | 3 +- .../net/zetetic/database/DatabaseUtils.java | 17 ++-- .../database/DefaultDatabaseErrorHandler.java | 7 +- .../java/net/zetetic/database/LogTarget.java | 6 ++ .../net/zetetic/database/LogcatTarget.java | 22 +++++ .../java/net/zetetic/database/Logger.java | 80 +++++++++++++++++++ .../java/net/zetetic/database/NoopTarget.java | 12 +++ .../database/sqlcipher/CloseGuard.java | 5 +- .../database/sqlcipher/SQLiteConnection.java | 12 +-- .../sqlcipher/SQLiteConnectionPool.java | 19 +++-- .../database/sqlcipher/SQLiteCursor.java | 12 +-- .../database/sqlcipher/SQLiteDatabase.java | 16 ++-- .../database/sqlcipher/SQLiteDebug.java | 11 ++- .../database/sqlcipher/SQLiteOpenHelper.java | 6 +- .../database/sqlcipher/SQLiteQuery.java | 4 +- .../sqlcipher/SQLiteQueryBuilder.java | 7 +- sqlcipher/src/main/jni/sqlcipher/ALog-priv.h | 14 +++- .../src/main/jni/sqlcipher/CursorWindow.cpp | 6 +- .../android_database_SQLiteConnection.cpp | 11 ++- .../android_database_SQLiteGlobal.cpp | 8 +- 22 files changed, 260 insertions(+), 74 deletions(-) create mode 100644 sqlcipher/src/main/java/net/zetetic/database/LogTarget.java create mode 100644 sqlcipher/src/main/java/net/zetetic/database/LogcatTarget.java create mode 100644 sqlcipher/src/main/java/net/zetetic/database/Logger.java create mode 100644 sqlcipher/src/main/java/net/zetetic/database/NoopTarget.java diff --git a/README.md b/README.md index c66cd20..184f9e9 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,34 @@ db = Room.databaseBuilder(context, AppDatabase.class, databaseFile.getAbsolutePa .openHelperFactory(factory).build(); ``` +### Logging + +Logging may occur in 3 distinct areas within this library: + +1. Within the Java client library +2. Within the JNI interop layer +3. Within SQLCipher core + +##### Java Client Logging + +By default, logging within the Java client library is routed to Logcat. If you wish to disable this logging entirely, you may utilize +the [`NoopTarget`](sqlcipher/src/main/java/net/zetetic/database/NoopTarget.java) instead: + +```java +Logger.setTarget(new NoopTarget()); +``` + +You can instead provide a custom logging target by registering a different target that implements the [`LogTarget`](sqlcipher/src/main/java/net/zetetic/database/LogTarget.java) interface. + +##### JNI Interop Layer + +There are two different compile-specific options available to alter the logging output from the JNI layer. To remove `INFO`, `DEBUG`, and `VERBOSE` log messages from the JNI layer, include `-DNDEBUG` with CFLAGS; this will allow `WARN` and `ERROR` logs to output to logcat. Alternatively, to exclude all log output from JNI, build the library using `-DSQLCIPHER_OMIT_LOG`. + +##### SQLCipher core + +To manage the logging produced from SQLCipher core, please review the runtime configurations: [`PRAGMA cipher_log`](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log), +[`PRAGMA cipher_log_level`](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log_level), and [`PRAGMA cipher_log_source`](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log_source). + ### Building ## Android NDK diff --git a/README.md.template b/README.md.template index 84a3061..1a215f0 100644 --- a/README.md.template +++ b/README.md.template @@ -76,6 +76,34 @@ db = Room.databaseBuilder(context, AppDatabase.class, databaseFile.getAbsolutePa .openHelperFactory(factory).build(); ``` +### Logging + +Logging may occur in 3 distinct areas within this library: + +1. Within the Java client library +2. Within the JNI interop layer +3. Within SQLCipher core + +##### Java Client Logging + +By default, logging within the Java client library is routed to Logcat. If you wish to disable this logging entirely, you may utilize +the [`NoopTarget`](sqlcipher/src/main/java/net/zetetic/database/NoopTarget.java) instead: + +```java +Logger.setTarget(new NoopTarget()); +``` + +You can instead provide a custom logging target by registering a different target that implements the [`LogTarget`](sqlcipher/src/main/java/net/zetetic/database/LogTarget.java) interface. + +##### JNI Interop Layer + +There are two different compile-specific options available to alter the logging output from the JNI layer. To remove `INFO`, `DEBUG`, and `VERBOSE` log messages from the JNI layer, include `-DNDEBUG` with CFLAGS; this will allow `WARN` and `ERROR` logs to output to logcat. Alternatively, to exclude all log output from JNI, build the library using `-DSQLCIPHER_OMIT_LOG`. + +##### SQLCipher core + +To manage the logging produced from SQLCipher core, please review the runtime configurations: [`PRAGMA cipher_log`](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log), +[`PRAGMA cipher_log_level`](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log_level), and [`PRAGMA cipher_log_source`](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log_source). + ### Building ## Android NDK diff --git a/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java b/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java index b0c7fa7..c8356fa 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java +++ b/sqlcipher/src/main/java/net/zetetic/database/AbstractCursor.java @@ -28,7 +28,6 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.util.Log; import java.lang.ref.WeakReference; @@ -257,7 +256,7 @@ public int getColumnIndex(String columnName) { final int periodIndex = columnName.lastIndexOf('.'); if (periodIndex != -1) { Exception e = new Exception(); - Log.e(TAG, "requesting column name with table name -- " + columnName, e); + Logger.e(TAG, "requesting column name with table name -- " + columnName, e); columnName = columnName.substring(periodIndex + 1); } diff --git a/sqlcipher/src/main/java/net/zetetic/database/DatabaseUtils.java b/sqlcipher/src/main/java/net/zetetic/database/DatabaseUtils.java index ae81357..4fc618a 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/DatabaseUtils.java +++ b/sqlcipher/src/main/java/net/zetetic/database/DatabaseUtils.java @@ -35,7 +35,6 @@ import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.text.TextUtils; -import android.util.Log; import android.database.Cursor; @@ -114,14 +113,14 @@ public static final void writeExceptionToParcel(Parcel reply, Exception e) { logException = false; } else { reply.writeException(e); - Log.e(TAG, "Writing exception to parcel", e); + Logger.e(TAG, "Writing exception to parcel", e); return; } reply.writeInt(code); reply.writeString(e.getMessage()); if (logException) { - Log.e(TAG, "Writing exception to parcel", e); + Logger.e(TAG, "Writing exception to parcel", e); } } @@ -1080,7 +1079,7 @@ private void buildSQL() throws SQLException { sb.append(sbv); mInsertSQL = sb.toString(); - if (DEBUG) Log.v(TAG, "insert statement is " + mInsertSQL); + if (DEBUG) Logger.v(TAG, "insert statement is " + mInsertSQL); } private SQLiteStatement getStatement(boolean allowReplace) throws SQLException { @@ -1124,13 +1123,13 @@ private long insertInternal(ContentValues values, boolean allowReplace) { try { SQLiteStatement stmt = getStatement(allowReplace); stmt.clearBindings(); - if (DEBUG) Log.v(TAG, "--- inserting in table " + mTableName); + if (DEBUG) Logger.v(TAG, "--- inserting in table " + mTableName); for (Map.Entry e: values.valueSet()) { final String key = e.getKey(); int i = getColumnIndex(key); DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue()); if (DEBUG) { - Log.v(TAG, "binding " + e.getValue() + " to column " + + Logger.v(TAG, "binding " + e.getValue() + " to column " + i + " (" + key + ")"); } } @@ -1138,7 +1137,7 @@ private long insertInternal(ContentValues values, boolean allowReplace) { mDb.setTransactionSuccessful(); return result; } catch (SQLException e) { - Log.e(TAG, "Error inserting " + values + " into table " + mTableName, e); + Logger.e(TAG, "Error inserting " + values + " into table " + mTableName, e); return -1; } finally { mDb.endTransaction(); @@ -1278,10 +1277,10 @@ public long execute() { + "execute"); } try { - if (DEBUG) Log.v(TAG, "--- doing insert or replace in table " + mTableName); + if (DEBUG) Logger.v(TAG, "--- doing insert or replace in table " + mTableName); return mPreparedStatement.executeInsert(); } catch (SQLException e) { - Log.e(TAG, "Error executing InsertHelper with table " + mTableName, e); + Logger.e(TAG, "Error executing InsertHelper with table " + mTableName, e); return -1; } finally { // you can only call this once per prepare diff --git a/sqlcipher/src/main/java/net/zetetic/database/DefaultDatabaseErrorHandler.java b/sqlcipher/src/main/java/net/zetetic/database/DefaultDatabaseErrorHandler.java index d78b8ef..95db892 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/DefaultDatabaseErrorHandler.java +++ b/sqlcipher/src/main/java/net/zetetic/database/DefaultDatabaseErrorHandler.java @@ -26,7 +26,6 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase; import android.database.sqlite.SQLiteException; -import android.util.Log; import android.util.Pair; /** @@ -57,7 +56,7 @@ public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { * is detected. */ public void onCorruption(SQLiteDatabase dbObj, SQLiteException exception) { - Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); + Logger.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); // If this is a SEE build, do not delete any database files. // It may be that the user has specified an incorrect password. @@ -107,12 +106,12 @@ private void deleteDatabaseFile(String fileName) { if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { return; } - Log.e(TAG, "deleting the database file: " + fileName); + Logger.e(TAG, "deleting the database file: " + fileName); try { SQLiteDatabase.deleteDatabase(new File(fileName)); } catch (Exception e) { /* print warning and ignore exception */ - Log.w(TAG, "delete failed: " + e.getMessage()); + Logger.w(TAG, "delete failed: " + e.getMessage()); } } } diff --git a/sqlcipher/src/main/java/net/zetetic/database/LogTarget.java b/sqlcipher/src/main/java/net/zetetic/database/LogTarget.java new file mode 100644 index 0000000..ddbf6ad --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/LogTarget.java @@ -0,0 +1,6 @@ +package net.zetetic.database; + +public interface LogTarget { + boolean isLoggable (String tag, int priority); + void log(int priority, String tag, String message, Throwable throwable); +} \ No newline at end of file diff --git a/sqlcipher/src/main/java/net/zetetic/database/LogcatTarget.java b/sqlcipher/src/main/java/net/zetetic/database/LogcatTarget.java new file mode 100644 index 0000000..33a40e8 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/LogcatTarget.java @@ -0,0 +1,22 @@ +package net.zetetic.database; + +import android.util.Log; + +public class LogcatTarget implements LogTarget { + + @Override + public boolean isLoggable(String tag, int priority) { + return Log.isLoggable(tag, priority); + } + + public void log(int priority, String tag, String message, Throwable throwable){ + switch (priority){ + case Logger.VERBOSE -> Log.v(tag, message, throwable); + case Logger.DEBUG -> Log.d(tag, message, throwable); + case Logger.INFO -> Log.i(tag, message, throwable); + case Logger.WARN -> Log.w(tag, message, throwable); + case Logger.ERROR -> Log.e(tag, message, throwable); + case Logger.ASSERT -> Log.wtf(tag, message, throwable); + } + } +} diff --git a/sqlcipher/src/main/java/net/zetetic/database/Logger.java b/sqlcipher/src/main/java/net/zetetic/database/Logger.java new file mode 100644 index 0000000..94673c0 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/Logger.java @@ -0,0 +1,80 @@ +package net.zetetic.database; + +public class Logger { + + static { + setTarget(new LogcatTarget()); + } + + public static final int VERBOSE = 2; + public static final int DEBUG = 3; + public static final int INFO = 4; + public static final int WARN = 5; + public static final int ERROR = 6; + public static final int ASSERT = 7; + + private static LogTarget target; + + public static void setTarget(LogTarget target){ + Logger.target = target; + } + + private static LogTarget getTarget(){ + if(Logger.target == null){ + setTarget(new NoopTarget()); + } + return Logger.target; + } + + public static boolean isLoggable(String tag, int priority){ + return getTarget().isLoggable(tag, priority); + } + + public static void i(String tag, String message) { + getTarget().log(Logger.INFO, tag, message, null); + } + + public static void i(String tag, String message, Throwable throwable) { + getTarget().log(Logger.INFO, tag, message, throwable); + } + + public static void d(String tag, String message) { + getTarget().log(Logger.DEBUG, tag, message, null); + } + + public static void d(String tag, String message, Throwable throwable) { + getTarget().log(Logger.DEBUG, tag, message, throwable); + } + + public static void e(String tag, String message) { + getTarget().log(Logger.ERROR, tag, message, null); + } + + public static void e(String tag, String message, Throwable throwable) { + getTarget().log(Logger.ERROR, tag, message, throwable); + } + + public static void v(String tag, String message) { + getTarget().log(Logger.VERBOSE, tag, message, null); + } + + public static void v(String tag, String message, Throwable throwable) { + getTarget().log(Logger.VERBOSE, tag, message, throwable); + } + + public static void w(String tag, String message) { + getTarget().log(Logger.WARN, tag, message, null); + } + + public static void w(String tag, String message, Throwable throwable) { + getTarget().log(Logger.WARN, tag, message, throwable); + } + + public static void wtf(String tag, String message) { + getTarget().log(Logger.ASSERT, tag, message, null); + } + + public static void wtf(String tag, String message, Throwable throwable) { + getTarget().log(Logger.ASSERT, tag, message, throwable); + } +} \ No newline at end of file diff --git a/sqlcipher/src/main/java/net/zetetic/database/NoopTarget.java b/sqlcipher/src/main/java/net/zetetic/database/NoopTarget.java new file mode 100644 index 0000000..534dfd6 --- /dev/null +++ b/sqlcipher/src/main/java/net/zetetic/database/NoopTarget.java @@ -0,0 +1,12 @@ +package net.zetetic.database; + +public class NoopTarget implements LogTarget { + + @Override + public boolean isLoggable(String tag, int priority) { + return false; + } + + @Override + public void log(int priority, String tag, String message, Throwable throwable) {} +} diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/CloseGuard.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/CloseGuard.java index 93aabc2..a3e1789 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/CloseGuard.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/CloseGuard.java @@ -19,7 +19,8 @@ */ package net.zetetic.database.sqlcipher; -import android.util.Log; + +import net.zetetic.database.Logger; /** * CloseGuard is a mechanism for flagging implicit finalizer cleanup of @@ -229,7 +230,7 @@ public static interface Reporter { */ private static final class DefaultReporter implements Reporter { @Override public void report (String message, Throwable allocationSite) { - Log.w(message, allocationSite); + android.util.Log.w(message, allocationSite); } } } diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnection.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnection.java index f2da861..130530a 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnection.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnection.java @@ -28,12 +28,12 @@ import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.os.SystemClock; -import android.util.Log; import android.util.LruCache; import android.util.Printer; import net.zetetic.database.CursorWindow; import net.zetetic.database.DatabaseUtils; +import net.zetetic.database.Logger; import net.zetetic.database.sqlcipher.SQLiteDebug.DbStats; import java.text.SimpleDateFormat; @@ -216,7 +216,7 @@ void close() { void changePassword(byte[] newPassword){ int result = nativeReKey(mConnectionPtr, newPassword); - Log.i(TAG, String.format("Database rekey operation returned:%s", result)); + Logger.i(TAG, String.format("Database rekey operation returned:%s", result)); if(result != 0){ throw new SQLiteException(String.format("Failed to rekey database, result code:%s", result)); } @@ -231,7 +231,7 @@ private void open() { } if(mConfiguration.password != null && mConfiguration.password.length > 0){ int rc = nativeKey(mConnectionPtr, mConfiguration.password); - Log.i(TAG, String.format("Database keying operation returned:%s", rc)); + Logger.i(TAG, String.format("Database keying operation returned:%s", rc)); } if(mConfiguration.databaseHook != null){ mConfiguration.databaseHook.postKey(this); @@ -372,7 +372,7 @@ private void setJournalMode(String newValue) { // If we don't change the journal mode, nothing really bad happens. // In the worst case, an application that enables WAL might not actually // get it, although it can still use connection pooling. - Log.w(TAG, "Could not change the database journal mode of '" + Logger.w(TAG, "Could not change the database journal mode of '" + mConfiguration.label + "' from '" + value + "' to '" + newValue + "' because the database is locked. This usually means that " + "there are other open connections to the database which prevents " @@ -1003,7 +1003,7 @@ private void releasePreparedStatement(PreparedStatement statement) { // which will in turn call finalizePreparedStatement() to finalize and // recycle the statement. if (DEBUG) { - Log.d(TAG, "Could not reset prepared statement due to an exception. " + Logger.d(TAG, "Could not reset prepared statement due to an exception. " + "Removing it from the cache. SQL: " + trimSqlForDisplay(statement.mSql), ex); } @@ -1462,7 +1462,7 @@ private void logOperationLocked(int cookie, String detail) { if (detail != null) { msg.append(", ").append(detail); } - Log.d(TAG, msg.toString()); + Logger.d(TAG, msg.toString()); } private int newOperationCookieLocked(int index) { diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnectionPool.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnectionPool.java index 5f117fb..98960a6 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnectionPool.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteConnectionPool.java @@ -20,13 +20,12 @@ package net.zetetic.database.sqlcipher; -import net.zetetic.database.sqlcipher.CloseGuard; +import net.zetetic.database.Logger; import net.zetetic.database.sqlcipher.SQLiteDebug.DbStats; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.SystemClock; -import android.util.Log; import android.util.Printer; import java.io.Closeable; @@ -231,7 +230,7 @@ private void dispose(boolean finalized) { final int pendingCount = mAcquiredConnections.size(); if (pendingCount != 0) { - Log.i(TAG, "The connection pool for " + mConfiguration.label + Logger.i(TAG, "The connection pool for " + mConfiguration.label + " has been closed but there are still " + pendingCount + " connections in use. They will be closed " + "as they are released back to the pool."); @@ -407,7 +406,7 @@ private boolean recycleConnectionLocked(SQLiteConnection connection, try { connection.reconfigure(mConfiguration); // might throw } catch (RuntimeException ex) { - Log.e(TAG, "Failed to reconfigure released connection, closing it: " + Logger.e(TAG, "Failed to reconfigure released connection, closing it: " + connection, ex); status = AcquiredConnectionStatus.DISCARD; } @@ -497,7 +496,7 @@ void onConnectionLeaked() { // several seconds while waiting for a leaked connection to be detected and recreated, // then perhaps its authors will have added incentive to fix the problem! - Log.w(TAG, "A SQLiteConnection object for database '" + Logger.w(TAG, "A SQLiteConnection object for database '" + mConfiguration.label + "' was leaked! Please fix your application " + "to end transactions in progress properly and to close the database " + "when it is no longer needed."); @@ -539,7 +538,7 @@ private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) try { connection.close(); // might throw } catch (RuntimeException ex) { - Log.e(TAG, "Failed to close connection, its fate is now in the hands " + Logger.e(TAG, "Failed to close connection, its fate is now in the hands " + "of the merciful GC: " + connection, ex); } } @@ -555,7 +554,7 @@ private void reconfigureAllConnectionsLocked() { try { mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw } catch (RuntimeException ex) { - Log.e(TAG, "Failed to reconfigure available primary connection, closing it: " + Logger.e(TAG, "Failed to reconfigure available primary connection, closing it: " + mAvailablePrimaryConnection, ex); closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); mAvailablePrimaryConnection = null; @@ -568,7 +567,7 @@ private void reconfigureAllConnectionsLocked() { try { connection.reconfigure(mConfiguration); // might throw } catch (RuntimeException ex) { - Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: " + Logger.e(TAG, "Failed to reconfigure available non-primary connection, closing it: " + connection, ex); closeConnectionAndLogExceptionsLocked(connection); mAvailableNonPrimaryConnections.remove(i--); @@ -797,7 +796,7 @@ private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) { } } - Log.w(TAG, msg.toString()); + Logger.w(TAG, msg.toString()); } // Can't throw. @@ -934,7 +933,7 @@ private void finishAcquireConnectionLocked(SQLiteConnection connection, int conn mAcquiredConnections.put(connection, AcquiredConnectionStatus.NORMAL); } catch (RuntimeException ex) { - Log.e(TAG, "Failed to prepare acquired connection for session, closing it: " + Logger.e(TAG, "Failed to prepare acquired connection for session, closing it: " + connection +", connectionFlags=" + connectionFlags); closeConnectionAndLogExceptionsLocked(connection); throw ex; // rethrow! diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java index 595e701..4b9454b 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteCursor.java @@ -20,11 +20,11 @@ package net.zetetic.database.sqlcipher; + import net.zetetic.database.AbstractWindowedCursor; import net.zetetic.database.CursorWindow; import net.zetetic.database.DatabaseUtils; - -import android.util.Log; +import net.zetetic.database.Logger; import java.util.HashMap; import java.util.Map; @@ -170,8 +170,8 @@ private void fillWindow(int requiredPos) { int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); mCursorWindowCapacity = mWindow.getNumRows(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "received count(*) from native_fill_window: " + mCount); + if (Logger.isLoggable(TAG, Logger.DEBUG)) { + Logger.d(TAG, "received count(*) from native_fill_window: " + mCount); } } else { int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, @@ -205,7 +205,7 @@ public int getColumnIndex(String columnName) { final int periodIndex = columnName.lastIndexOf('.'); if (periodIndex != -1) { Exception e = new Exception(); - Log.e(TAG, "requesting column name with table name -- " + columnName, e); + Logger.e(TAG, "requesting column name with table name -- " + columnName, e); columnName = columnName.substring(periodIndex + 1); } @@ -261,7 +261,7 @@ public boolean requery() { return super.requery(); } catch (IllegalStateException e) { // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); + Logger.w(TAG, "requery() failed " + e.getMessage(), e); return false; } } diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java index 2b5755f..1a26f71 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDatabase.java @@ -25,6 +25,7 @@ import net.zetetic.database.DatabaseErrorHandler; import net.zetetic.database.DatabaseUtils; import net.zetetic.database.DefaultDatabaseErrorHandler; +import net.zetetic.database.Logger; import net.zetetic.database.sqlcipher.SQLiteDebug.DbStats; import android.database.SQLException; @@ -35,7 +36,6 @@ import android.os.OperationCanceledException; import android.text.TextUtils; import android.util.EventLog; -import android.util.Log; import android.util.Pair; import android.util.Printer; @@ -1016,7 +1016,7 @@ private void open() { openInner(); } } catch (SQLiteException ex) { - Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex); + Logger.e(TAG, "Failed to open database '" + getLabel() + "'.", ex); close(); throw ex; } @@ -1636,7 +1636,7 @@ public long insert(String table, String nullColumnHack, ContentValues values) { try { return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); } catch (SQLException e) { - Log.e(TAG, "Error inserting", e); + Logger.e(TAG, "Error inserting", e); return -1; } } @@ -1684,7 +1684,7 @@ public long replace(String table, String nullColumnHack, ContentValues initialVa return insertWithOnConflict(table, nullColumnHack, initialValues, CONFLICT_REPLACE); } catch (SQLException e) { - Log.e(TAG, "Error inserting", e); + Logger.e(TAG, "Error inserting", e); return -1; } } @@ -2272,15 +2272,15 @@ public boolean enableWriteAheadLogging() { } if (mConfigurationLocked.isInMemoryDb()) { - Log.i(TAG, "can't enable WAL for memory databases."); + Logger.i(TAG, "can't enable WAL for memory databases."); return false; } // make sure this database has NO attached databases because sqlite's write-ahead-logging // doesn't work for databases with attached databases if (mHasAttachedDbsLocked) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "this database: " + mConfigurationLocked.label + if (Logger.isLoggable(TAG, Logger.DEBUG)) { + Logger.d(TAG, "this database: " + mConfigurationLocked.label + " has attached databases. can't enable WAL."); } return false; @@ -2479,7 +2479,7 @@ public boolean isDatabaseIntegrityOk() { String rslt = prog.simpleQueryForString(); if (!rslt.equalsIgnoreCase("ok")) { // integrity_checker failed on main or attached databases - Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt); + Logger.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt); return false; } } finally { diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDebug.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDebug.java index 8184e01..4ea08df 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDebug.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteDebug.java @@ -22,11 +22,10 @@ import java.util.ArrayList; -import android.os.Build; -/* import android.os.SystemProperties; */ -import android.util.Log; import android.util.Printer; +import net.zetetic.database.Logger; + /** * Provides debugging info about all SQLite databases running in the current process. * @@ -41,7 +40,7 @@ public final class SQLiteDebug { * Enable using "adb shell setprop log.tag.SQLiteLog VERBOSE". */ public static final boolean DEBUG_SQL_LOG = - Log.isLoggable("SQLiteLog", Log.VERBOSE); + Logger.isLoggable("SQLiteLog", Logger.VERBOSE); /** * Controls the printing of SQL statements as they are executed. @@ -49,7 +48,7 @@ public final class SQLiteDebug { * Enable using "adb shell setprop log.tag.SQLiteStatements VERBOSE". */ public static final boolean DEBUG_SQL_STATEMENTS = - Log.isLoggable("SQLiteStatements", Log.VERBOSE); + Logger.isLoggable("SQLiteStatements", Logger.VERBOSE); /** * Controls the printing of wall-clock time taken to execute SQL statements @@ -58,7 +57,7 @@ public final class SQLiteDebug { * Enable using "adb shell setprop log.tag.SQLiteTime VERBOSE". */ public static final boolean DEBUG_SQL_TIME = - Log.isLoggable("SQLiteTime", Log.VERBOSE); + Logger.isLoggable("SQLiteTime", Logger.VERBOSE); /** * True to enable database performance testing instrumentation. diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteOpenHelper.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteOpenHelper.java index c639c7f..9d9f4fd 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteOpenHelper.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteOpenHelper.java @@ -23,10 +23,10 @@ import android.content.Context; import net.zetetic.database.DatabaseErrorHandler; +import net.zetetic.database.Logger; import net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteException; -import android.util.Log; import androidx.sqlite.db.SupportSQLiteOpenHelper; @@ -363,7 +363,7 @@ instance has not yet been created via getWritableDatabase(). This allows if (writable) { throw ex; } - Log.e(TAG, "Couldn't open " + mName + Logger.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", ex); final String path = mContext.getDatabasePath(mName).getPath(); db = SQLiteDatabase.openDatabase(path, mPassword, mFactory, @@ -414,7 +414,7 @@ instance has not yet been created via getWritableDatabase(). This allows onOpen(db); if (db.isReadOnly()) { - Log.w(TAG, "Opened " + mName + " in read-only mode"); + Logger.w(TAG, "Opened " + mName + " in read-only mode"); } mDatabase = db; diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java index 7ff8a61..aae818b 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQuery.java @@ -24,9 +24,9 @@ import android.database.sqlite.SQLiteException; import android.os.CancellationSignal; import android.os.OperationCanceledException; -import android.util.Log; import net.zetetic.database.CursorWindow; +import net.zetetic.database.Logger; /** * Represents a query that reads the resulting rows into a {@link SQLiteQuery}. @@ -74,7 +74,7 @@ window, startPos, requiredPos, countAllRows, getConnectionFlags(), onCorruption(ex); throw ex; } catch (SQLiteException ex) { - Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql()); + Logger.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql()); throw ex; } finally { window.releaseReference(); diff --git a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQueryBuilder.java b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQueryBuilder.java index 5d364bc..0e06c1f 100644 --- a/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQueryBuilder.java +++ b/sqlcipher/src/main/java/net/zetetic/database/sqlcipher/SQLiteQueryBuilder.java @@ -26,7 +26,8 @@ import android.os.OperationCanceledException; import android.provider.BaseColumns; import android.text.TextUtils; -import android.util.Log; + +import net.zetetic.database.Logger; import java.util.Iterator; import java.util.Map; @@ -397,8 +398,8 @@ public Cursor query(SQLiteDatabase db, String[] projectionIn, projectionIn, selection, groupBy, having, sortOrder, limit); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Performing query: " + sql); + if (Logger.isLoggable(TAG, Logger.DEBUG)) { + Logger.d(TAG, "Performing query: " + sql); } return db.rawQueryWithFactory( mFactory, sql, selectionArgs, diff --git a/sqlcipher/src/main/jni/sqlcipher/ALog-priv.h b/sqlcipher/src/main/jni/sqlcipher/ALog-priv.h index 18d0aac..ce3b709 100644 --- a/sqlcipher/src/main/jni/sqlcipher/ALog-priv.h +++ b/sqlcipher/src/main/jni/sqlcipher/ALog-priv.h @@ -44,7 +44,7 @@ #endif #ifndef ALOGV -#if LOG_NDEBUG +#if defined(LOG_NDEBUG) || defined(SQLCIPHER_OMIT_LOG) #define ALOGV(...) ((void)0) #else #define ALOGV(...) ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) @@ -52,7 +52,7 @@ #endif #ifndef ALOGD -#if LOG_NDEBUG +#if defined(LOG_NDEBUG) || defined(SQLCIPHER_OMIT_LOG) #define ALOGD(...) ((void)0) #else #define ALOGD(...) ((void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) @@ -60,7 +60,7 @@ #endif #ifndef ALOGI -#if LOG_NDEBUG +#if defined(LOG_NDEBUG) || defined(SQLCIPHER_OMIT_LOG) #define ALOGI(...) ((void)0) #else #define ALOGI(...) ((void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) @@ -68,12 +68,20 @@ #endif #ifndef ALOGW +#if defined(SQLCIPHER_OMIT_LOG) +#define ALOGW(...) ((void)0) +#else #define ALOGW(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) #endif +#endif #ifndef ALOGE +#if defined(SQLCIPHER_OMIT_LOG) +#define ALOGE(...) ((void)0) +#else #define ALOGE(...) ((void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) #endif +#endif /* ** Not quite the same as the core android LOG_FATAL_IF (which also diff --git a/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp index 65c7c84..c338e07 100644 --- a/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp +++ b/sqlcipher/src/main/jni/sqlcipher/CursorWindow.cpp @@ -129,14 +129,14 @@ namespace android { ? 2048 * 1024 : mSize * 2; uint32_t freeOffset = mHeader->freeOffset; - ALOGW("Request to resize CursorWindow allocation: current window size %zu bytes, " + ALOGI("Request to resize CursorWindow allocation: current window size %zu bytes, " "free space %zu bytes, new window size %zu bytes", mSize, freeSpace(), newSize); if((mData = realloc(mData, newSize))){ mHeader = static_cast(mData); mHeader->freeOffset = freeOffset; mSize = newSize; - ALOGW("Resized CursorWindow allocation: current window size %zu bytes", + ALOGI("Resized CursorWindow allocation: current window size %zu bytes", newSize); return OK; } else { @@ -160,7 +160,7 @@ namespace android { if(result == OK){ return alloc(size, aligned); } - ALOGW("Window is full: requested allocation %zu bytes, " + ALOGI("Window is full: requested allocation %zu bytes, " "free space %zu bytes, window size %zu bytes", size, freeSpace(), mSize); return 0; diff --git a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp index dbbe2a3..fe82135 100644 --- a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp +++ b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteConnection.cpp @@ -39,6 +39,9 @@ // Set to 1 to use UTF16 storage for localized indexes. #define UTF16_STORAGE 0 +#undef LOG_TAG +#define LOG_TAG SQLITE_LOG_TAG + namespace android { /* Busy timeout in milliseconds. @@ -96,15 +99,15 @@ struct SQLiteConnection { // Called each time a statement begins execution, when tracing is enabled. static void sqliteTraceCallback(void *data, const char *sql) { auto* connection = static_cast(data); - ALOG(LOG_VERBOSE, SQLITE_TRACE_TAG, "%s: \"%s\"\n", - connection->label.c_str(), sql); + ALOGV("%s: \"%s\"\n", + connection->label.c_str(), sql); } // Called each time a statement finishes execution, when profiling is enabled. static void sqliteProfileCallback(void *data, const char *sql, sqlite3_uint64 tm) { auto* connection = static_cast(data); - ALOG(LOG_VERBOSE, SQLITE_PROFILE_TAG, "%s: \"%s\" took %0.3f ms\n", - connection->label.c_str(), sql, tm * 0.000001f); + ALOGV("%s: \"%s\" took %0.3f ms\n", + connection->label.c_str(), sql, tm * 0.000001f); } // Called after each SQLite VM instruction when cancelation is enabled. diff --git a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteGlobal.cpp b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteGlobal.cpp index aa6ab07..de8e697 100644 --- a/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteGlobal.cpp +++ b/sqlcipher/src/main/jni/sqlcipher/android_database_SQLiteGlobal.cpp @@ -35,6 +35,8 @@ namespace android { // a long time. static const int SOFT_HEAP_LIMIT = 8 * 1024 * 1024; +#undef LOG_TAG +#define LOG_TAG SQLITE_LOG_TAG // Called each time a message is logged. static void sqliteLogCallback(void* data, int err, const char* msg) { @@ -43,12 +45,12 @@ static void sqliteLogCallback(void* data, int err, const char* msg) { if (errType == 0 || errType == SQLITE_CONSTRAINT || errType == SQLITE_SCHEMA || errType == SQLITE_NOTICE || err == SQLITE_WARNING_AUTOINDEX) { if (verboseLog) { - ALOG(LOG_VERBOSE, SQLITE_LOG_TAG, "(%d) %s\n", err, msg); + ALOGV("(%d) %s\n", err, msg); } } else if (errType == SQLITE_WARNING) { - ALOG(LOG_WARN, SQLITE_LOG_TAG, "(%d) %s\n", err, msg); + ALOGW("(%d) %s\n", err, msg); } else { - ALOG(LOG_ERROR, SQLITE_LOG_TAG, "(%d) %s\n", err, msg); + ALOGE("(%d) %s\n", err, msg); } }