diff --git a/README.md b/README.md index 67d34cf0..e05e3ff2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ --- dependencies on debian: -`sudo apt install libglib2.0-dev libgtk-4-dev libasound2-dev libopenxr-dev` +`sudo apt install libglib2.0-dev libgtk-4-dev libasound2-dev libopenxr-dev libportal-dev libsqlite3-dev` build instructions: 1. compile and install https://gitlab.com/Mis012/dalvik_standalone (art branch) (you can run it from builddir, but it requires some additional env variables) diff --git a/meson.build b/meson.build index 5d4d0f27..4ed5c372 100644 --- a/meson.build +++ b/meson.build @@ -64,6 +64,8 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/android_view_Window.c', 'src/api-impl-jni/util.c', 'src/api-impl-jni/android_graphics_Canvas.c', + 'src/api-impl-jni/database/android_database_SQLiteCommon.c', + 'src/api-impl-jni/database/android_database_SQLiteConnection.c', 'src/api-impl-jni/drawables/ninepatch.c', 'src/api-impl-jni/android_content_res_AssetManager.c', 'src/api-impl-jni/audio/android_media_AudioTrack.c', @@ -93,7 +95,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ install: true, install_dir : get_option('libdir') / 'java/dex/android_translation_layer/natives', dependencies: [ - dependency('gtk4'), dependency('gl'), dependency('egl'), dependency('wayland-client'), dependency('jni'), dependency('libportal') + dependency('gtk4'), dependency('gl'), dependency('egl'), dependency('wayland-client'), dependency('jni'), dependency('libportal'), dependency('sqlite3') ], link_with: [ libandroid_so ], link_args: [ diff --git a/src/api-impl-jni/database/android_database_SQLiteCommon.c b/src/api-impl-jni/database/android_database_SQLiteCommon.c new file mode 100644 index 00000000..bb63e148 --- /dev/null +++ b/src/api-impl-jni/database/android_database_SQLiteCommon.c @@ -0,0 +1,141 @@ +/* + * 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 to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ +/* +** Rewritten from C++ to C for Android Translation Layer: +*/ + +#include +#include + +#include "android_database_SQLiteCommon.h" + +/* throw a SQLiteException with a message appropriate for the error in handle */ +void throw_sqlite3_exception_handle(JNIEnv* env, sqlite3* handle) { + throw_sqlite3_exception_handle_message(env, handle, NULL); +} + +/* throw a SQLiteException with the given message */ +void throw_sqlite3_exception_message(JNIEnv* env, const char* message) { + throw_sqlite3_exception_handle_message(env, NULL, message); +} + +/* throw a SQLiteException with a message appropriate for the error in handle + concatenated with the given message + */ +void throw_sqlite3_exception_handle_message(JNIEnv* env, sqlite3* handle, const char* message) { + if (handle) { + // get the error code and message from the SQLite connection + // the error message may contain more information than the error code + // because it is based on the extended error code rather than the simplified + // error code that SQLite normally returns. + throw_sqlite3_exception_errcode_message(env, sqlite3_extended_errcode(handle), + sqlite3_errmsg(handle), message); + } else { + // we use SQLITE_OK so that a generic SQLiteException is thrown; + // any code not specified in the switch statement below would do. + throw_sqlite3_exception_errcode_message(env, SQLITE_OK, "unknown error", message); + } +} + +/* throw a SQLiteException for a given error code + * should only be used when the database connection is not available because the + * error information will not be quite as rich */ +void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* message) { + throw_sqlite3_exception_errcode_message(env, errcode, "unknown error", message); +} + +/* throw a SQLiteException for a given error code, sqlite3message, and + user message + */ +void throw_sqlite3_exception_errcode_message(JNIEnv* env, int errcode, + const char* sqlite3Message, const char* message) { + const char* exceptionClass; + switch (errcode & 0xff) { /* mask off extended error code */ + case SQLITE_IOERR: + exceptionClass = "android/database/sqlite/SQLiteDiskIOException"; + break; + case SQLITE_CORRUPT: + case SQLITE_NOTADB: // treat "unsupported file format" error as corruption also + exceptionClass = "android/database/sqlite/SQLiteDatabaseCorruptException"; + break; + case SQLITE_CONSTRAINT: + exceptionClass = "android/database/sqlite/SQLiteConstraintException"; + break; + case SQLITE_ABORT: + exceptionClass = "android/database/sqlite/SQLiteAbortException"; + break; + case SQLITE_DONE: + exceptionClass = "android/database/sqlite/SQLiteDoneException"; + sqlite3Message = NULL; // SQLite error message is irrelevant in this case + break; + case SQLITE_FULL: + exceptionClass = "android/database/sqlite/SQLiteFullException"; + break; + case SQLITE_MISUSE: + exceptionClass = "android/database/sqlite/SQLiteMisuseException"; + break; + case SQLITE_PERM: + exceptionClass = "android/database/sqlite/SQLiteAccessPermException"; + break; + case SQLITE_BUSY: + exceptionClass = "android/database/sqlite/SQLiteDatabaseLockedException"; + break; + case SQLITE_LOCKED: + exceptionClass = "android/database/sqlite/SQLiteTableLockedException"; + break; + case SQLITE_READONLY: + exceptionClass = "android/database/sqlite/SQLiteReadOnlyDatabaseException"; + break; + case SQLITE_CANTOPEN: + exceptionClass = "android/database/sqlite/SQLiteCantOpenDatabaseException"; + break; + case SQLITE_TOOBIG: + exceptionClass = "android/database/sqlite/SQLiteBlobTooBigException"; + break; + case SQLITE_RANGE: + exceptionClass = "android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException"; + break; + case SQLITE_NOMEM: + exceptionClass = "android/database/sqlite/SQLiteOutOfMemoryException"; + break; + case SQLITE_MISMATCH: + exceptionClass = "android/database/sqlite/SQLiteDatatypeMismatchException"; + break; + case SQLITE_INTERRUPT: + exceptionClass = "android/os/OperationCanceledException"; + break; + default: + exceptionClass = "android/database/sqlite/SQLiteException"; + break; + } + + if (sqlite3Message) { + char *zFullmsg = sqlite3_mprintf( + "%s (code %d)%s%s", sqlite3Message, errcode, + (message ? ": " : ""), (message ? message : "") + ); + printf("throwing new %s\n", exceptionClass); + (*env)->ThrowNew(env, (*env)->FindClass(env, exceptionClass), zFullmsg); + sqlite3_free(zFullmsg); + } else { + printf("throwing new %s\n", exceptionClass); + (*env)->ThrowNew(env, (*env)->FindClass(env, exceptionClass), message); + } +} diff --git a/src/api-impl-jni/database/android_database_SQLiteCommon.h b/src/api-impl-jni/database/android_database_SQLiteCommon.h new file mode 100644 index 00000000..4082e771 --- /dev/null +++ b/src/api-impl-jni/database/android_database_SQLiteCommon.h @@ -0,0 +1,50 @@ +/* + * 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 to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +#ifndef _ANDROID_DATABASE_SQLITE_COMMON_H +#define _ANDROID_DATABASE_SQLITE_COMMON_H + +#include + +#include + +// Special log tags defined in SQLiteDebug.java. +#define SQLITE_LOG_TAG "SQLiteLog" +#define SQLITE_TRACE_TAG "SQLiteStatements" +#define SQLITE_PROFILE_TAG "SQLiteTime" + +/* throw a SQLiteException with a message appropriate for the error in handle */ +void throw_sqlite3_exception_handle(JNIEnv* env, sqlite3* handle); + +/* throw a SQLiteException with the given message */ +void throw_sqlite3_exception_message(JNIEnv* env, const char* message); + +/* throw a SQLiteException with a message appropriate for the error in handle + concatenated with the given message + */ +void throw_sqlite3_exception_handle_message(JNIEnv* env, sqlite3* handle, const char* message); + +/* throw a SQLiteException for a given error code */ +void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* message); + +void throw_sqlite3_exception_errcode_message(JNIEnv* env, int errcode, + const char* sqlite3Message, const char* message); + +#endif // _ANDROID_DATABASE_SQLITE_COMMON_H diff --git a/src/api-impl-jni/database/android_database_SQLiteConnection.c b/src/api-impl-jni/database/android_database_SQLiteConnection.c new file mode 100644 index 00000000..5ecbdc09 --- /dev/null +++ b/src/api-impl-jni/database/android_database_SQLiteConnection.c @@ -0,0 +1,739 @@ +/* + * Copyright (C) 2011 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 to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ +/* +** Rewritten from C++ to C for Android Translation Layer: +*/ + +#include +#include +#include +#include +#include + +#include + +#include "android_database_SQLiteCommon.h" +#include "../generated_headers/android_database_sqlite_SQLiteConnection.h" +#include "../defines.h" +#include "../util.h" + +enum { + // Open flags. + // Must be kept in sync with the constants defined in SQLiteDatabase.java. + OPEN_READWRITE = 0x00000000, + OPEN_READONLY = 0x00000001, + OPEN_READ_MASK = 0x00000001, + NO_LOCALIZED_COLLATORS = 0x00000010, + CREATE_IF_NECESSARY = 0x10000000, +}; + +struct SQLiteConnection { + sqlite3* db; + int openFlags; + char *path; + char *label; + + volatile bool canceled; +}; + +/* Busy timeout in milliseconds. + * If another connection (possibly in another process) has the database locked for + * longer than this amount of time then SQLite will generate a SQLITE_BUSY error. + * The SQLITE_BUSY error is then raised as a SQLiteDatabaseLockedException. + * + * In ordinary usage, busy timeouts are quite rare. Most databases only ever + * have a single open connection at a time unless they are using WAL. When using + * WAL, a timeout could occur if one connection is busy performing an auto-checkpoint + * operation. The busy timeout needs to be long enough to tolerate slow I/O write + * operations but not so long as to cause the application to hang indefinitely if + * there is a problem acquiring a database lock. + */ +static const int BUSY_TIMEOUT_MS = 2500; + +/* +** This function is a collation sequence callback equivalent to the built-in +** BINARY sequence. +** +** Stock Android uses a modified version of sqlite3.c that calls out to a module +** named "sqlite3_android" to add extra built-in collations and functions to +** all database handles. Specifically, collation sequence "LOCALIZED". For now, +** this module does not include sqlite3_android (since it is difficult to build +** with the NDK only). Instead, this function is registered as "LOCALIZED" for all +** new database handles. +*/ +static int coll_localized( + void *not_used, + int nKey1, const void *pKey1, + int nKey2, const void *pKey2 +){ + int rc, n; + n = nKey1label, sql); +} + +// Called each time a statement finishes execution, when profiling is enabled. +static void sqliteProfileCallback(void *data, const char *sql, sqlite3_uint64 tm) { + // struct SQLiteConnection* connection = data; + // printf(SQLITE_PROFILE_TAG " %s: \"%s\" took %0.3f ms\n", + // connection->label, sql, tm * 0.000001f); +} + +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativeOpen(JNIEnv *env, jclass clazz, jstring pathStr, jint openFlags, + jstring labelStr, jboolean enableTrace, jboolean enableProfile) { + int sqliteFlags; + if (openFlags & CREATE_IF_NECESSARY) { + sqliteFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; + } else if (openFlags & OPEN_READONLY) { + sqliteFlags = SQLITE_OPEN_READONLY; + } else { + sqliteFlags = SQLITE_OPEN_READWRITE; + } + + const char* pathChars = (*env)->GetStringUTFChars(env, pathStr, NULL); + char *path = strdup(pathChars); + (*env)->ReleaseStringUTFChars(env, pathStr, pathChars); + + const char* labelChars = (*env)->GetStringUTFChars(env, labelStr, NULL); + char *label = strdup(labelChars); + (*env)->ReleaseStringUTFChars(env, labelStr, labelChars); + + sqlite3* db; + int err = sqlite3_open_v2(path, &db, sqliteFlags, NULL); + if (err != SQLITE_OK) { + throw_sqlite3_exception_errcode(env, err, "Could not open database"); + return 0; + } + err = sqlite3_create_collation(db, "localized", SQLITE_UTF8, 0, coll_localized); + if (err != SQLITE_OK) { + throw_sqlite3_exception_errcode(env, err, "Could not register collation"); + sqlite3_close(db); + return 0; + } + + // Check that the database is really read/write when that is what we asked for. + if ((sqliteFlags & SQLITE_OPEN_READWRITE) && sqlite3_db_readonly(db, NULL)) { + throw_sqlite3_exception_handle_message(env, db, "Could not open the database in read/write mode."); + sqlite3_close(db); + return 0; + } + + // Set the default busy handler to retry automatically before returning SQLITE_BUSY. + err = sqlite3_busy_timeout(db, BUSY_TIMEOUT_MS); + if (err != SQLITE_OK) { + throw_sqlite3_exception_handle_message(env, db, "Could not set busy timeout"); + sqlite3_close(db); + return 0; + } + + // Create wrapper object. + struct SQLiteConnection* connection = malloc(sizeof(struct SQLiteConnection)); + connection->db = db; + connection->openFlags = openFlags; + connection->path = path; + connection->label = label; + + // Enable tracing and profiling if requested. + if (enableTrace) { + sqlite3_trace(db, &sqliteTraceCallback, connection); + } + if (enableProfile) { + sqlite3_profile(db, &sqliteProfileCallback, connection); + } + + // printf("Opened connection %p with label '%s'\n", db, label); + return _INTPTR(connection); +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeClose(JNIEnv *env, jclass clazz, jlong connectionPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + + if (connection) { + // printf("Closing connection %p", connection->db); + int err = sqlite3_close(connection->db); + if (err != SQLITE_OK) { + // This can happen if sub-objects aren't closed first. Make sure the caller knows. + fprintf(stderr, "sqlite3_close(%p) failed: %d\n", connection->db, err); + throw_sqlite3_exception_handle_message(env, connection->db, "Count not close db."); + return; + } + + free(connection->path); + free(connection->label); + free(connection); + } +} + +// Called each time a custom function is evaluated. +static void sqliteCustomFunctionCallback(sqlite3_context *context, int argc, sqlite3_value **argv) { + JNIEnv* env = get_jni_env(); + + // 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 = sqlite3_user_data(context); + jobject functionObj = (*env)->NewLocalRef(env, functionObjGlobal); + + jobjectArray argsArray = (*env)->NewObjectArray(env, argc, (*env)->FindClass(env, "java/lang/String"), NULL); + if (argsArray) { + for (int i = 0; i < argc; i++) { + const jchar* arg = sqlite3_value_text16(argv[i]); + if (!arg) { + fprintf(stderr, "NULL argument in custom_function_callback. This should not happen."); + } else { + size_t argLen = sqlite3_value_bytes16(argv[i]) / sizeof(jchar); + jstring argStr = (*env)->NewString(env, arg, argLen); + if (!argStr) { + goto error; // out of memory error + } + (*env)->SetObjectArrayElement(env, argsArray, i, argStr); + (*env)->DeleteLocalRef(env, argStr); + } + } + + jclass custom_function_class = (*env)->FindClass(env, "android/database/sqlite/SQLiteCustomFunction"); + // TODO: Support functions that return values. + (*env)->CallVoidMethod(env, functionObj, + _METHOD(custom_function_class, "dispatchCallback", "([Ljava/lang/String;)V"), argsArray); + +error: + (*env)->DeleteLocalRef(env, argsArray); + } + + (*env)->DeleteLocalRef(env, functionObj); + + if ((*env)->ExceptionCheck(env)) { + fprintf(stderr, "An exception was thrown by custom SQLite function."); + /* LOGE_EX(env); */ + (*env)->ExceptionClear(env); + } +} + +// Called when a custom function is destroyed. +static void sqliteCustomFunctionDestructor(void* data) { + jobject functionObjGlobal = data; + JNIEnv* env = get_jni_env(); + (*env)->DeleteGlobalRef(env, functionObjGlobal); +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeRegisterCustomFunction(JNIEnv* env, jclass clazz, jlong connectionPtr, jobject functionObj) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + + jclass custom_function_class = (*env)->FindClass(env, "android/database/sqlite/SQLiteCustomFunction"); + + jstring nameStr = (*env)->GetObjectField(env, functionObj, _FIELD_ID(custom_function_class, "name", "Ljava/lang/String;")); + jint numArgs = (*env)->GetIntField(env, functionObj, _FIELD_ID(custom_function_class, "numArgs", "I")); + + jobject functionObjGlobal = (*env)->NewGlobalRef(env, functionObj); + + const char* name = (*env)->GetStringUTFChars(env, nameStr, NULL); + int err = sqlite3_create_function_v2(connection->db, name, numArgs, SQLITE_UTF16, functionObjGlobal, + &sqliteCustomFunctionCallback, NULL, NULL, &sqliteCustomFunctionDestructor); + (*env)->ReleaseStringUTFChars(env, nameStr, name); + + if (err != SQLITE_OK) { + fprintf(stderr, "sqlite3_create_function returned %d", err); + (*env)->DeleteGlobalRef(env, functionObjGlobal); + throw_sqlite3_exception_handle(env, connection->db); + return; + } +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeRegisterLocalizedCollators(JNIEnv* env, jclass clazz, jlong connectionPtr, jstring localeStr) { + /* Localized collators are not supported. */ +} + +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativePrepareStatement(JNIEnv* env, jclass clazz, jlong connectionPtr, jstring sqlString) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + + jsize sqlLength = (*env)->GetStringLength(env, sqlString); + const jchar* sql = (*env)->GetStringCritical(env, sqlString, NULL); + sqlite3_stmt* statement; + int err = sqlite3_prepare16_v2(connection->db, + sql, sqlLength * sizeof(jchar), &statement, NULL); + (*env)->ReleaseStringCritical(env, sqlString, sql); + + if (err != SQLITE_OK) { + // Error messages like 'near ")": syntax error' are not + // always helpful enough, so construct an error string that + // includes the query itself. + const char *query = (*env)->GetStringUTFChars(env, sqlString, NULL); + char *message = (char*) malloc(strlen(query) + 50); + if (message) { + strcpy(message, ", while compiling: "); // less than 50 chars + strcat(message, query); + } + (*env)->ReleaseStringUTFChars(env, sqlString, query); + throw_sqlite3_exception_handle_message(env, connection->db, message); + free(message); + return 0; + } + + // printf("Prepared statement %p on connection %p\n", statement, connection->db); + return _INTPTR(statement); +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeFinalizeStatement(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + // struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(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 + // is always finalized regardless. + // printf("Finalized statement %p on connection %p\n", statement, connection->db); + sqlite3_finalize(statement); +} + +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeGetParameterCount(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + sqlite3_stmt* statement = _PTR(statementPtr); + + return sqlite3_bind_parameter_count(statement); +} + +JNIEXPORT jboolean JNICALL Java_android_database_sqlite_SQLiteConnection_nativeIsReadOnly(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + sqlite3_stmt* statement = _PTR(statementPtr); + + return sqlite3_stmt_readonly(statement) != 0; +} + +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeGetColumnCount(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + sqlite3_stmt* statement = _PTR(statementPtr); + + return sqlite3_column_count(statement); +} + +JNIEXPORT jstring JNICALL Java_android_database_sqlite_SQLiteConnection_nativeGetColumnName(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index) { + sqlite3_stmt* statement = _PTR(statementPtr); + + const jchar* name = (sqlite3_column_name16(statement, index)); + if (name) { + size_t length = 0; + while (name[length]) { + length += 1; + } + return (*env)->NewString(env, name, length); + } + return NULL; +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindNull(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + int err = sqlite3_bind_null(statement, index); + if (err != SQLITE_OK) { + throw_sqlite3_exception_handle_message(env, connection->db, NULL); + } +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindLong(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index, jlong value) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + int err = sqlite3_bind_int64(statement, index, value); + if (err != SQLITE_OK) { + throw_sqlite3_exception_handle_message(env, connection->db, NULL); + } +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindDouble(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index, jdouble value) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + int err = sqlite3_bind_double(statement, index, value); + if (err != SQLITE_OK) { + throw_sqlite3_exception_handle_message(env, connection->db, NULL); + } +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindString(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index, jstring valueString) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + jsize valueLength = (*env)->GetStringLength(env, valueString); + const jchar* value = (*env)->GetStringCritical(env, valueString, NULL); + int err = sqlite3_bind_text16(statement, index, value, valueLength * sizeof(jchar), + SQLITE_TRANSIENT); + (*env)->ReleaseStringCritical(env, valueString, value); + if (err != SQLITE_OK) { + throw_sqlite3_exception_handle_message(env, connection->db, NULL); + } +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindBlob(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr, jint index, jbyteArray valueArray) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + jsize valueLength = (*env)->GetArrayLength(env, valueArray); + jbyte* value = ((*env)->GetPrimitiveArrayCritical(env, valueArray, NULL)); + int err = sqlite3_bind_blob(statement, index, value, valueLength, SQLITE_TRANSIENT); + (*env)->ReleasePrimitiveArrayCritical(env, valueArray, value, JNI_ABORT); + if (err != SQLITE_OK) { + throw_sqlite3_exception_handle_message(env, connection->db, NULL); + } +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeResetStatementAndClearBindings(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + int err = sqlite3_reset(statement); + if (err == SQLITE_OK) { + err = sqlite3_clear_bindings(statement); + } + if (err != SQLITE_OK) { + throw_sqlite3_exception_handle_message(env, connection->db, NULL); + } +} + +static int executeNonQuery(JNIEnv* env, struct SQLiteConnection* connection, sqlite3_stmt* statement) { + int err; + while( SQLITE_ROW==(err=sqlite3_step(statement)) ); + if( err!=SQLITE_DONE ){ + throw_sqlite3_exception_handle(env, connection->db); + } + return err; +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecute(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + executeNonQuery(env, connection, statement); +} + +static int executeOneRowQuery(JNIEnv* env, struct SQLiteConnection* connection, sqlite3_stmt* statement) { + int err = sqlite3_step(statement); + if (err != SQLITE_ROW) { + throw_sqlite3_exception_handle(env, connection->db); + } + return err; +} + +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForLong(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + int err = executeOneRowQuery(env, connection, statement); + if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) { + return sqlite3_column_int64(statement, 0); + } + return -1; +} + +JNIEXPORT jstring JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForString(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + int err = executeOneRowQuery(env, connection, statement); + if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) { + const jchar* text = (sqlite3_column_text16(statement, 0)); + if (text) { + size_t length = sqlite3_column_bytes16(statement, 0) / sizeof(jchar); + return (*env)->NewString(env, text, length); + } + } + return NULL; +} + +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForChangedRowCount(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + int err = executeNonQuery(env, connection, statement); + return err == SQLITE_DONE ? sqlite3_changes(connection->db) : -1; +} + +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForLastInsertedRowId(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + sqlite3_stmt* statement = _PTR(statementPtr); + + int err = executeNonQuery(env, connection, statement); + return err == SQLITE_DONE && sqlite3_changes(connection->db) > 0 + ? sqlite3_last_insert_rowid(connection->db) : -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 */ +}; + +/* +** 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 *env, + jobject win, + int iRow, + sqlite3_stmt *pStmt, + struct CWMethod *aMethod +){ + int nCol = sqlite3_column_count(pStmt); + int i; + jboolean bOk; + + bOk = (*env)->CallBooleanMethod(env, win, aMethod[CW_ALLOCROW].id); + for(i=0; bOk && iCallBooleanMethod(env, win, aMethod[CW_PUTNULL].id, iRow, i); + break; + } + + case SQLITE_INTEGER: { + jlong val = sqlite3_column_int64(pStmt, i); + bOk = (*env)->CallBooleanMethod(env, win, aMethod[CW_PUTLONG].id, val, iRow, i); + break; + } + + case SQLITE_FLOAT: { + jdouble val = sqlite3_column_double(pStmt, i); + bOk = (*env)->CallBooleanMethod(env, 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 = (*env)->NewString(env, pStr, nStr); + bOk = (*env)->CallBooleanMethod(env, win, aMethod[CW_PUTSTRING].id, val, iRow, i); + (*env)->DeleteLocalRef(env, 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 = (*env)->NewByteArray(env, n); + (*env)->SetByteArrayRegion(env, val, 0, n, p); + bOk = (*env)->CallBooleanMethod(env, win, aMethod[CW_PUTBLOB].id, val, iRow, i); + (*env)->DeleteLocalRef(env, val); + break; + } + } + + if( bOk==0 ){ + (*env)->CallVoidMethod(env, win, aMethod[CW_FREELASTROW].id); + } + } + + return bOk; +} + +static jboolean setWindowNumColumns( + JNIEnv *env, + jobject win, + sqlite3_stmt *pStmt, + struct CWMethod *aMethod +){ + int nCol; + + (*env)->CallVoidMethod(env, win, aMethod[CW_CLEAR].id); + nCol = sqlite3_column_count(pStmt); + return (*env)->CallBooleanMethod(env, win, aMethod[CW_SETNUMCOLUMNS].id, (jint)nCol); +} + +/* +** 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. +*/ +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForCursorWindow( + JNIEnv *env, + 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 +) { + sqlite3_stmt *pStmt = _PTR(statementPtr); + + struct 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 nRow; + jboolean bOk; + int iStart; /* First row copied to CursorWindow */ + + /* Locate all required CursorWindow methods. */ + cls = (*env)->FindClass(env, "android/database/CursorWindow"); + for(i=0; i<(sizeof(aMethod)/sizeof(struct CWMethod)); i++){ + aMethod[i].id = (*env)->GetMethodID(env, cls, aMethod[i].zName, aMethod[i].zSig); + if( aMethod[i].id==NULL ){ + char msgBuf[512]; + snprintf(msgBuf, sizeof(msgBuf), "Failed to find method CursorWindow.%s()", aMethod[i].zName); + (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/Exception"), msgBuf); + return 0; + } + } + + + /* Set the number of columns in the window */ + bOk = setWindowNumColumns(env, win, pStmt, aMethod); + if( bOk==0 ) return 0; + + nRow = 0; + iStart = startPos; + while( sqlite3_step(pStmt)==SQLITE_ROW ){ + /* Only copy in rows that occur at or after row index iStart. */ + if( nRow>=iStart && bOk ){ + bOk = copyRowToWindow(env, 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(env, win, pStmt, aMethod); + if( bOk==0 ){ + sqlite3_reset(pStmt); + return 0; + } + iStart = nRow; + bOk = copyRowToWindow(env, win, (nRow - iStart), pStmt, aMethod); + } + + /* 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_handle(env, sqlite3_db_handle(pStmt)); + return 0; + } + + jlong lRet = ((jlong)iStart) << 32 | ((jlong)nRow); + return lRet; +} + +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeGetDbLookaside(JNIEnv* env, jobject clazz, jlong connectionPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + + int cur = -1; + int unused; + sqlite3_db_status(connection->db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur, &unused, 0); + return cur; +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeCancel(JNIEnv* env, jobject clazz, jlong connectionPtr) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + connection->canceled = true; +} + +// Called after each SQLite VM instruction when cancelation is enabled. +static int sqliteProgressHandlerCallback(void* data) { + struct SQLiteConnection* connection = (data); + return connection->canceled; +} + +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeResetCancel(JNIEnv* env, jobject clazz, jlong connectionPtr, jboolean cancelable) { + struct SQLiteConnection* connection = _PTR(connectionPtr); + connection->canceled = false; + + if (cancelable) { + sqlite3_progress_handler(connection->db, 4, sqliteProgressHandlerCallback, + connection); + } else { + sqlite3_progress_handler(connection->db, 0, NULL, NULL); + } +} + +JNIEXPORT jboolean JNICALL Java_android_database_sqlite_SQLiteConnection_nativeHasCodec(JNIEnv* env, jobject clazz){ +#ifdef SQLITE_HAS_CODEC + return true; +#else + return false; +#endif +}