diff --git a/sqlite-android/src/androidTest/java/io/requery/android/database/DatabaseGeneralTest.java b/sqlite-android/src/androidTest/java/io/requery/android/database/DatabaseGeneralTest.java index 043f84a5..b49d4f35 100644 --- a/sqlite-android/src/androidTest/java/io/requery/android/database/DatabaseGeneralTest.java +++ b/sqlite-android/src/androidTest/java/io/requery/android/database/DatabaseGeneralTest.java @@ -39,6 +39,10 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -162,6 +166,34 @@ public void callback(Args args, Result result) { assertSame(null, cursor.getString(0)); } + @MediumTest + @Test + public void testSetUpdateHook() { + // Initialize AtomicReferences with a default value + AtomicInteger calledOperation = new AtomicInteger(); + AtomicReference calledDatabaseName = new AtomicReference<>(""); + AtomicReference calledTableName = new AtomicReference<>(""); + AtomicLong calledRowId = new AtomicLong(); + + // Set up the update hook + mDatabase.setUpdateHook((operationType, databaseName, tableName, rowId) -> { + calledOperation.set(operationType); + calledDatabaseName.set(databaseName); + calledTableName.set(tableName); + calledRowId.set(rowId); + }); + + // Execute SQL statements + mDatabase.execSQL("CREATE TABLE testUpdateHook (_id INTEGER PRIMARY KEY, data TEXT);"); + mDatabase.execSQL("INSERT INTO testUpdateHook (data) VALUES ('newValue');"); + + // Verify that the update hook was called correctly + assertEquals(18, calledOperation.get()); + assertEquals("main", calledDatabaseName.get()); + assertEquals("testUpdateHook", calledTableName.get()); + assertEquals(1, calledRowId.get()); + } + @MediumTest @Test public void testVersion() { diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnection.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnection.java index c408b865..d3d4749b 100644 --- a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnection.java +++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnection.java @@ -170,6 +170,8 @@ private static native long nativeExecuteForCursorWindow( private static native boolean nativeHasCodec(); private static native void nativeLoadExtension(long connectionPtr, String file, String proc); + private static native void nativeRegisterUpdateHook(long connectionPtr, SQLiteUpdateHook updateCallback); + public static boolean hasCodec(){ return nativeHasCodec(); } private SQLiteConnection(SQLiteConnectionPool pool, @@ -255,6 +257,12 @@ private void open() { for (SQLiteCustomExtension extension : mConfiguration.customExtensions) { nativeLoadExtension(mConnectionPtr, extension.path, extension.entryPoint); } + + final SQLiteUpdateHook sqliteUpdateHook = mConfiguration.sqliteUpdateHook; + + if (sqliteUpdateHook != null) { + nativeRegisterUpdateHook(mConnectionPtr, sqliteUpdateHook); + } } private void dispose(boolean finalized) { @@ -456,6 +464,11 @@ void reconfigure(SQLiteDatabaseConfiguration configuration) { } } + final SQLiteUpdateHook updateHook = configuration.sqliteUpdateHook; + if (updateHook != null) { + nativeRegisterUpdateHook(mConnectionPtr, updateHook); + } + // Remember what changed. boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled != mConfiguration.foreignKeyConstraintsEnabled; diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabase.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabase.java index aca107f4..1a452de7 100644 --- a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabase.java +++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabase.java @@ -940,6 +940,21 @@ public void addFunction(String name, int numArgs, Function function, int flags) } } + public void setUpdateHook(SQLiteUpdateHook updateHook) { + synchronized (mLock) { + throwIfNotOpenLocked(); + + mConfigurationLocked.sqliteUpdateHook = updateHook; + + try { + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } catch (RuntimeException ex) { + mConfigurationLocked.sqliteUpdateHook = null; + throw ex; + } + } + } + /** * Gets the database version. * diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabaseConfiguration.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabaseConfiguration.java index 2087f2bb..4c9123dc 100644 --- a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabaseConfiguration.java +++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabaseConfiguration.java @@ -63,6 +63,8 @@ public final class SQLiteDatabaseConfiguration { */ public @SQLiteDatabase.OpenFlags int openFlags; + public SQLiteUpdateHook sqliteUpdateHook; + /** * The maximum size of the prepared statement cache for each database connection. * Must be non-negative. @@ -184,6 +186,7 @@ void updateParametersFrom(SQLiteDatabaseConfiguration other) { customExtensions.addAll(other.customExtensions); functions.clear(); functions.addAll(other.functions); + sqliteUpdateHook = other.sqliteUpdateHook; } /** diff --git a/sqlite-android/src/main/jni/sqlite/android_database_SQLiteConnection.cpp b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteConnection.cpp index bd7ff7b8..d199c951 100644 --- a/sqlite-android/src/main/jni/sqlite/android_database_SQLiteConnection.cpp +++ b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteConnection.cpp @@ -221,6 +221,42 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { } } +static void sqliteUpdateHookCallback(void *pArg, int operationType, const char *databaseName, const char *tableName, sqlite3_int64 rowId) { + JNIEnv* env = 0; + gpJavaVM->GetEnv((void**)&env, JNI_VERSION_1_4); + + jobject updateCallbackObjGlobal = reinterpret_cast(pArg); + jobject updateCallbackObj = env->NewLocalRef(updateCallbackObjGlobal); + + // TODO: Do something magical + // Get the Java class and method ID + jclass updateHookManagerClass = env->GetObjectClass(updateCallbackObj); + jmethodID onUpdateMethod = env->GetMethodID(updateHookManagerClass, "onUpdateFromNative", "(ILjava/lang/String;Ljava/lang/String;J)V"); + + if (onUpdateMethod != NULL) { + // Create Java strings from C strings + jstring dbName = env->NewStringUTF(databaseName); + jstring tblName = env->NewStringUTF(tableName); + + // Call the Java method + env->CallVoidMethod(updateCallbackObj, onUpdateMethod, operationType, dbName, tblName, rowId); + + // Clean up local references + env->DeleteLocalRef(dbName); + env->DeleteLocalRef(tblName); + } else { + ALOGE("Failed to find onUpdateFromNative method"); + } + + env->DeleteLocalRef(updateCallbackObj); + + if (env->ExceptionCheck()) { + ALOGE("An exception was thrown by custom update callback."); + /* LOGE_EX(env); */ + env->ExceptionClear(); + } +} + // Called each time a custom function is evaluated. static void sqliteCustomFunctionCallback(sqlite3_context *context, int argc, sqlite3_value **argv) { @@ -373,6 +409,20 @@ static void nativeRegisterFunction(JNIEnv *env, jclass clazz, jlong connectionPt } } +static void nativeRegisterUpdateHook(JNIEnv* env, jclass clazz, jlong connectionPtr, + jobject updateCallbackObj) { + SQLiteConnection* connection = reinterpret_cast(connectionPtr); + + jobject updateCallbackObjGlobal = env->NewGlobalRef(updateCallbackObj); + + if (updateCallbackObjGlobal == NULL) { + ALOGE("Failed to create global reference for callback object"); + return; + } + + sqlite3_update_hook(connection->db, sqliteUpdateHookCallback, reinterpret_cast(updateCallbackObjGlobal)); +} + static void nativeRegisterLocalizedCollators(JNIEnv* env, jclass clazz, jlong connectionPtr, jstring localeStr) { SQLiteConnection* connection = reinterpret_cast(connectionPtr); @@ -988,6 +1038,8 @@ static JNINativeMethod sMethods[] = (void*)nativeHasCodec }, { "nativeLoadExtension", "(JLjava/lang/String;Ljava/lang/String;)V", (void*)nativeLoadExtension }, + { "nativeRegisterUpdateHook", "(JLio/requery/android/database/sqlite/SQLiteUpdateHook;)V", + (void*)nativeRegisterUpdateHook }, }; int register_android_database_SQLiteConnection(JNIEnv *env)