diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cf01c776 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +charset = utf-8 + +[*.{java,cpp,h,aidl,xml,md}] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf + +[*.gradle] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index dbb4b150..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,11 +0,0 @@ -### Expected Behavior - -### Actual Behavior - -### Steps to Reproduce - -SQLCipher version (can be identified by executing `PRAGMA cipher_version;`): - -SQLCipher for Android version: - -*Note:* If you are not posting a specific issue for the SQLCipher library, please consider posting your question to the SQLCipher [discuss site](https://discuss.zetetic.net/c/sqlcipher). Thanks! diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..e62c3968 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 Community support + url: https://discuss.zetetic.net/c/sqlcipher/5 + about: Integration problem or question about SQLCipher for Android, feel free to ask here. + - name: 🔨 Build issue + url: https://discuss.zetetic.net/c/sqlcipher/5 + about: Experience an issue building SQLCipher for Android? Start here. + - name: 📃 SQLCipher documentation + url: https://www.zetetic.net/sqlcipher/sqlcipher-api/ + about: SQLCipher documentation can be found here. + - name: 📖 Contribution instructions + url: https://www.zetetic.net/contributions/ + about: Want to contribute to SQLCipher for Android? Start here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 00000000..4c78327f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,21 @@ +--- +name: 🛠️ Bug report +about: Create a report about a software defect +title: '' +labels: '' +assignees: '' +--- + +### Expected Behavior + +### Actual Behavior + +### Steps to Reproduce + +SQLCipher version (can be identified by executing `PRAGMA cipher_version;`): + +SQLCipher for Android version: + +Are you able to reproduce this issue within the SQLCipher for Android [test suite](https://github.com/sqlcipher/sqlcipher-android-tests)? + +*Note:* If you are not posting a specific issue for the SQLCipher library, please post your question to the SQLCipher [discuss site](https://discuss.zetetic.net/c/sqlcipher). Thanks! diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..ff6a36ab --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,21 @@ +# Configuration for probot-stale - https://github.com/probot/stale +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 14 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 14 +# Issues with these labels will never be considered stale +exemptLabels: + - bug + - enhancement + - security +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + Hello, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. + You may also label this issue as "bug", "enhancement", or "security" and I will leave it open. + Thank you for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to reopen with up-to-date information. +only: issues diff --git a/.gitignore b/.gitignore index 2bb58ef0..94a7c0c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,14 @@ +.gradle +build .DS_Store -build.xml -proguard-project.txt -local.properties -external/android-libs -obj -jni/libs -bin -libs -gen -*.zip -.d -jni/libs* -TAGS \ No newline at end of file +android-database-sqlcipher/src/main/external/sqlcipher +android-database-sqlcipher/src/main/external/openssl +android-database-sqlcipher/src/main/external/android-libs/ +android-database-sqlcipher/.externalNativeBuild/ +android-database-sqlcipher/src/main/libs* +android-database-sqlcipher/src/main/obj +android-database-sqlcipher/src/main/external/openssl-*/ +android-database-sqlcipher/src/main/cpp/sqlite3.[c,h] +.idea/ +*.iml +local.properties \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 66da6caf..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +0,0 @@ -[submodule "external/sqlcipher"] - path = external/sqlcipher - url = https://github.com/sqlcipher/sqlcipher.git -[submodule "external/openssl"] - path = external/openssl - url = https://github.com/openssl/openssl.git diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index 1e3a8cd6..00000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/Makefile b/Makefile index ca5f8003..7a42f371 100644 --- a/Makefile +++ b/Makefile @@ -1,115 +1,100 @@ -.DEFAULT_GOAL := all -BIN_DIR := ${CURDIR}/bin -JNI_DIR := ${CURDIR}/jni -LIBS_DIR := ${CURDIR}/libs -EXTERNAL_DIR := ${CURDIR}/external -SQLCIPHER_DIR := ${CURDIR}/external/sqlcipher -LICENSE := ${CURDIR}/SQLCIPHER_LICENSE -SQLCIPHER_CFLAGS := \ - -DSQLITE_HAS_CODEC \ - -DSQLITE_SOUNDEX \ - -DHAVE_USLEEP=1 \ - -DSQLITE_TEMP_STORE=3 \ - -DSQLITE_THREADSAFE=1 \ - -DSQLITE_DEFAULT_JOURNAL_SIZE_LIMIT=1048576 \ - -DNDEBUG=1 \ - -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1 \ - -DSQLITE_ENABLE_LOAD_EXTENSION \ - -DSQLITE_ENABLE_COLUMN_METADATA \ - -DSQLITE_ENABLE_UNLOCK_NOTIFY \ - -DSQLITE_ENABLE_RTREE \ - -DSQLITE_ENABLE_STAT3 \ - -DSQLITE_ENABLE_STAT4 \ - -DSQLITE_ENABLE_JSON1 \ - -DSQLITE_ENABLE_FTS3_PARENTHESIS \ - -DSQLITE_ENABLE_FTS4 \ - -DSQLITE_ENABLE_FTS5 \ - -DSQLCIPHER_CRYPTO_OPENSSL - -.PHONY: clean develop-zip release-zip release - -init: init-environment build-openssl-libraries - -init-environment: - git submodule update --init - android update project -p ${CURDIR} - -build-openssl-libraries: - ./build-openssl-libraries.sh - -build-amalgamation: - cd ${SQLCIPHER_DIR} && \ - ./configure --enable-tempstore=yes \ - --with-crypto-lib=none \ - CFLAGS="${SQLCIPHER_CFLAGS}" && \ - make sqlite3.c - -build-java: - ant release - -build-native-32: - cd ${JNI_DIR} && \ - ndk-build V=1 --environment-overrides NDK_LIBS_OUT=$(JNI_DIR)/libs32 \ - NDK_APPLICATION_MK=$(JNI_DIR)/Application32.mk \ - SQLCIPHER_CFLAGS="${SQLCIPHER_CFLAGS}" - -build-native-64: - cd ${JNI_DIR} && \ - ndk-build V=1 --environment-overrides NDK_LIBS_OUT=$(JNI_DIR)/libs64 \ - NDK_APPLICATION_MK=$(JNI_DIR)/Application64.mk \ - SQLCIPHER_CFLAGS="${SQLCIPHER_CFLAGS}" - -build-native: build-native-32 build-native-64 - -clean-java: - ant clean - rm -rf ${LIBS_DIR} - -clean-ndk: - -cd ${JNI_DIR} && \ - ndk-build clean --environment-overrides NDK_LIBS_OUT=$(JNI_DIR)/libs32 \ - NDK_APPLICATION_MK=$(JNI_DIR)/Application32.mk && \ - ndk-build clean --environment-overrides NDK_LIBS_OUT=$(JNI_DIR)/libs64 \ - NDK_APPLICATION_MK=$(JNI_DIR)/Application64.mk - -rm -rf ${JNI_DIR}/libs32 ${JNI_DIR}/libs64 - -clean: clean-ndk clean-java - -cd ${SQLCIPHER_DIR} && \ - make clean - rm sqlcipher-for-android-*.zip - -distclean: clean - rm -rf ${EXTERNAL_DIR}/android-libs - -copy-libs: - -cp -R ${JNI_DIR}/libs32/* ${JNI_DIR}/libs64/* ${LIBS_DIR} - -release-aar: - -rm ${LIBS_DIR}/sqlcipher.jar - -rm ${LIBS_DIR}/sqlcipher-javadoc.jar - mvn package - -develop-zip: LATEST_TAG := $(shell git rev-parse --short HEAD) -develop-zip: SECOND_LATEST_TAG ?= $(shell git tag | sort -r | head -1) -develop-zip: release - -release-zip: LATEST_TAG := $(shell git tag | sort -r | head -1) -release-zip: SECOND_LATEST_TAG := $(shell git tag | sort -r | head -2 | tail -1) -release-zip: release - -release: - $(eval RELEASE_DIR := sqlcipher-for-android-${LATEST_TAG}) - $(eval README := ${RELEASE_DIR}/README) - $(eval CHANGE_LOG_HEADER := "Changes included in the ${LATEST_TAG} release of SQLCipher for Android:") - -rm -rf ${RELEASE_DIR} - -rm ${RELEASE_DIR}.zip - mkdir -p ${RELEASE_DIR}/docs - cp -R ${LIBS_DIR}/* ${RELEASE_DIR} - cp -R ${BIN_DIR}/javadoc/* ${RELEASE_DIR}/docs - cp ${LICENSE} ${RELEASE_DIR} - printf "%s\n\n" ${CHANGE_LOG_HEADER} > ${README} - git log --pretty=format:' * %s' ${SECOND_LATEST_TAG}..${LATEST_TAG} >> ${README} - find ${RELEASE_DIR} | sort -u | zip -@9 ${RELEASE_DIR}.zip - rm -rf ${RELEASE_DIR} - -all: build-amalgamation build-native build-java copy-libs +.POSIX: +.PHONY: init clean distclean build-openssl build publish-local-snapshot \ + publish-local-release publish-remote-snapshot public-remote-release check +GRADLE = ./gradlew + +clean: + $(GRADLE) clean + +distclean: + $(GRADLE) distclean \ + -PsqlcipherRoot="$(SQLCIPHER_ROOT)" + +build-openssl: + $(GRADLE) buildOpenSSL + +check: + $(GRADLE) check + +format: + $(GRADLE) editorconfigFormat + +build-debug: + $(GRADLE) android-database-sqlcipher:bundleDebugAar \ + -PdebugBuild=true \ + -PsqlcipherRoot="$(SQLCIPHER_ROOT)" \ + -PopensslRoot="$(OPENSSL_ROOT)" \ + -PopensslAndroidNativeRoot="$(OPENSSL_ANDROID_LIB_ROOT)" \ + -PsqlcipherCFlags="$(SQLCIPHER_CFLAGS)" \ + -PsqlcipherAndroidClientVersion="$(SQLCIPHER_ANDROID_VERSION)" + +build-release: + $(GRADLE) android-database-sqlcipher:bundleReleaseAar \ + -PdebugBuild=false \ + -PsqlcipherRoot="$(SQLCIPHER_ROOT)" \ + -PopensslRoot="$(OPENSSL_ROOT)" \ + -PopensslAndroidNativeRoot="$(OPENSSL_ANDROID_LIB_ROOT)" \ + -PsqlcipherCFlags="$(SQLCIPHER_CFLAGS)" \ + -PsqlcipherAndroidClientVersion="$(SQLCIPHER_ANDROID_VERSION)" + +publish-local-snapshot: + @ $(collect-signing-info) \ + $(GRADLE) \ + -PpublishSnapshot=true \ + -PpublishLocal=true \ + -PsigningKeyId="$$gpgKeyId" \ + -PsigningKeyRingFile="$$gpgKeyRingFile" \ + -PsigningKeyPassword="$$gpgPassword" \ + uploadArchives + +publish-local-release: + @ $(collect-signing-info) \ + $(GRADLE) \ + -PpublishSnapshot=false \ + -PpublishLocal=true \ + -PsigningKeyId="$$gpgKeyId" \ + -PsigningKeyRingFile="$$gpgKeyRingFile" \ + -PsigningKeyPassword="$$gpgPassword" \ + uploadArchives + +publish-remote-snapshot: + @ $(collect-signing-info) \ + $(collect-nexus-info) \ + $(GRADLE) \ + -PpublishSnapshot=true \ + -PpublishLocal=false \ + -PsigningKeyId="$$gpgKeyId" \ + -PsigningKeyRingFile="$$gpgKeyRingFile" \ + -PsigningKeyPassword="$$gpgPassword" \ + -PnexusUsername="$$nexusUsername" \ + -PnexusPassword="$$nexusPassword" \ + uploadArchives + +publish-remote-release: + @ $(collect-signing-info) \ + $(collect-nexus-info) \ + $(GRADLE) \ + -PpublishSnapshot=false \ + -PpublishLocal=false \ + -PdebugBuild=false \ + -PsigningKeyId="$$gpgKeyId" \ + -PsigningKeyRingFile="$$gpgKeyRingFile" \ + -PsigningKeyPassword="$$gpgPassword" \ + -PnexusUsername="$$nexusUsername" \ + -PnexusPassword="$$nexusPassword" \ + -PsqlcipherRoot="$(SQLCIPHER_ROOT)" \ + -PopensslRoot="$(OPENSSL_ROOT)" \ + -PopensslAndroidLibRoot="$(OPENSSL_ANDROID_LIB_ROOT)" \ + -PsqlcipherCFlags="$(SQLCIPHER_CFLAGS)" \ + -PsqlcipherAndroidClientVersion="$(SQLCIPHER_ANDROID_VERSION)" \ + android-database-sqlcipher:publish + +collect-nexus-info := \ + read -p "Enter Nexus username:" nexusUsername; \ + stty -echo; read -p "Enter Nexus password:" nexusPassword; stty echo; + +collect-signing-info := \ + read -p "Enter GPG signing key id:" gpgKeyId; \ + read -p "Enter full path to GPG keyring file \ + (possibly ${HOME}/.gnupg/secring.gpg)" gpgKeyRingFile; \ + stty -echo; read -p "Enter GPG password:" gpgPassword; stty echo; diff --git a/README.md b/README.md new file mode 100644 index 00000000..68637ce0 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +### Deprecated Library + +The `android-database-sqlcipher` project has been [officially deprecated](https://www.zetetic.net/blog/2023/08/31/sqlcipher-4.5.5-release#sqlcipher-android-455). The long-term replacement is [`sqlcipher-android`](https://github.com/sqlcipher/sqlcipher-android). Instructions for migrating from `android-database-sqlcipher` to `sqlcipher-android`may be found [here](https://www.zetetic.net/sqlcipher/sqlcipher-for-android-migration/). + + +### Download Source and Binaries + +The latest AAR binary package information can be [here](https://www.zetetic.net/sqlcipher/open-source), the source can be found [here](https://github.com/sqlcipher/android-database-sqlcipher). +

+ +### Compatibility + +SQLCipher for Android runs on Android from 5.0 (API 21), for `armeabi-v7a`, `x86`, `x86_64`, and `arm64_v8a` architectures. + +### Contributions + +We welcome contributions, to contribute to SQLCipher for Android, a [contributor agreement](https://www.zetetic.net/contributions/) needs to be submitted. All submissions should be based on the `master` branch. + +### An Illustrative Terminal Listing + +A typical SQLite database in unencrypted, and visually parseable even as encoded text. The following example shows the difference between hexdumps of a standard SQLite database and one implementing SQLCipher. + +``` +~ sjlombardo$ hexdump -C sqlite.db +00000000 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 |SQLite format 3.| +… +000003c0 65 74 32 74 32 03 43 52 45 41 54 45 20 54 41 42 |et2t2.CREATE TAB| +000003d0 4c 45 20 74 32 28 61 2c 62 29 24 01 06 17 11 11 |LE t2(a,b)$…..| +… +000007e0 20 74 68 65 20 73 68 6f 77 15 01 03 01 2f 01 6f | the show…./.o| +000007f0 6e 65 20 66 6f 72 20 74 68 65 20 6d 6f 6e 65 79 |ne for the money| + +~ $ sqlite3 sqlcipher.db +sqlite> PRAGMA KEY=’test123′; +sqlite> CREATE TABLE t1(a,b); +sqlite> INSERT INTO t1(a,b) VALUES (‘one for the money’, ‘two for the show’); +sqlite> .quit + +~ $ hexdump -C sqlcipher.db +00000000 84 d1 36 18 eb b5 82 90 c4 70 0d ee 43 cb 61 87 |.?6.?..?p.?C?a.| +00000010 91 42 3c cd 55 24 ab c6 c4 1d c6 67 b4 e3 96 bb |.B?..?| +00000bf0 8e 99 ee 28 23 43 ab a4 97 cd 63 42 8a 8e 7c c6 |..?(#C??.?cB..|?| + +~ $ sqlite3 sqlcipher.db +sqlite> SELECT * FROM t1; +Error: file is encrypted or is not a database +``` +(example courtesy of SQLCipher) + +### Application Integration + +You have a two main options for using SQLCipher for Android in your app: + +- Using it with Room or other consumers of the `androidx.sqlite` API + +- Using the native SQLCipher for Android classes + +In both cases, you will need to add a dependency on `net.zetetic:android-database-sqlcipher`, +such as having the following line in your module's `build.gradle` `dependencies` +closure: + +```gradle +implementation "net.zetetic:android-database-sqlcipher:4.5.3" +implementation "androidx.sqlite:sqlite:2.1.0" +``` + +(replacing `4.5.3` with the version you want) + + + +#### Using SQLCipher for Android With Room + +SQLCipher for Android has a `SupportFactory` class in the `net.sqlcipher.database` package +that can be used to configure Room to use SQLCipher for Android. + +There are three `SupportFactory` constructors: + +- `SupportFactory(byte[] passphrase)` +- `SupportFactory(byte[] passphrase, SQLiteDatabaseHook hook)` +- `SupportFactory(byte[] passphrase, SQLiteDatabaseHook hook, boolean clearPassphrase)` + +All three take a `byte[]` to use as the passphrase (if you have a `char[]`, use +`SQLiteDatabase.getBytes()` to get a suitable `byte[]` to use). + +Two offer a `SQLiteDatabaseHook` parameter that you can use +for executing SQL statements before or after the passphrase is used to key +the database. + +The three-parameter constructor also offers `clearPassphrase`, which defaults +to `true` in the other two constructors. If `clearPassphrase` is set to `true`, +this will zero out the bytes of the `byte[]` after we open the database. This +is safest from a security standpoint, but it does mean that the `SupportFactory` +instance is a single-use object. Attempting to reuse the `SupportFactory` +instance later will result in being unable to open the database, because the +passphrase will be wrong. If you think that you might need to reuse the +`SupportFactory` instance, pass `false` for `clearPassphrase`. + +Then, pass your `SupportFactory` to `openHelperFactory()` on your `RoomDatabase.Builder`: + +```java +final byte[] passphrase = SQLiteDatabase.getBytes(userEnteredPassphrase); +final SupportFactory factory = new SupportFactory(passphrase); +final SomeDatabase room = Room.databaseBuilder(activity, SomeDatabase.class, DB_NAME) + .openHelperFactory(factory) + .build(); +``` + +Now, Room will make all of its database requests using SQLCipher for Android instead +of the framework copy of SQLCipher. + +Note that `SupportFactory` should work with other consumers of the `androidx.sqlite` API; +Room is merely a prominent example. + +#### Using SQLCipher for Android's Native API + +If you have existing SQLite code using classes like `SQLiteDatabase` and `SQLiteOpenHelper`, +converting your code to use SQLCipher for Android mostly is a three-step process: + +1. Replace all `android.database.sqlite.*` `import` statements with ones that +use `net.sqlcipher.database.*` (e.g., convert `android.database.sqlite.SQLiteDatabase` +to `net.sqlcipher.database.SQLiteDatabase`) + +2. Before attempting to open a database, call `SQLiteDatabase.loadLibs()`, passing +in a `Context` (e.g., add this to `onCreate()` of your `Application` subclass, using +the `Application` itself as the `Context`) + +3. When opening a database (e.g., `SQLiteDatabase.openOrCreateDatabase()`), pass +in the passphrase as a `char[]` or `byte[]` + +The rest of your code may not need any changes. + +An article covering both integration of SQLCipher into an Android application as well as building the source can be found [here](https://www.zetetic.net/sqlcipher/sqlcipher-for-android/). + +### ProGuard + +For applications which utilize ProGuard, a few additional rules must be included when using SQLCipher for Android. These rules instruct ProGuard to omit the renaming of the internal SQLCipher classes which are used via lookup from the JNI layer. It is worth noting that since SQLCipher or Android is based on open source code there is little value in obfuscating the library anyway. The more important use of ProGuard is to protect your application code and business logic. + +``` +-keep,includedescriptorclasses class net.sqlcipher.** { *; } +-keep,includedescriptorclasses interface net.sqlcipher.** { *; } +``` + +### Building + +In order to build `android-database-sqlcipher` from source you will need both the Android SDK, Gradle, Android NDK, SQLCipher core source directory, and an OpenSSL source directory. We currently recommend using Android NDK LTS version `23.0.7599858`. + +To complete the `make` command, the `ANDROID_NDK_HOME` environment variable must be defined which should point to your NDK root. Once you have cloned the repo, change directory into the root of the repository and run the following commands: + +``` +SQLCIPHER_ROOT=/some/path/to/sqlcipher-folder \ +OPENSSL_ROOT=/some/path/to/openssl-folder \ +SQLCIPHER_CFLAGS="-DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=2" \ +SQLCIPHER_ANDROID_VERSION="4.5.3" \ +make build-release +``` + +You may specify other build flags/features within `SQLCIPHER_CFLAGS`, however, specifying `-DSQLITE_HAS_CODEC` and `-DSQLITE_TEMP_STORE` is necessary in the list of flags. + +### License + +The Android support libraries are licensed under Apache 2.0, in line with the Android OS code on which they are based. The SQLCipher code itself is licensed under a BSD-style license from Zetetic LLC. Finally, the original SQLite code itself is in the public domain. diff --git a/README.org b/README.org deleted file mode 100644 index 37c26395..00000000 --- a/README.org +++ /dev/null @@ -1,74 +0,0 @@ -*** Download Source and Binaries - - The latest binary packages for developers, with the jar’s, .so’s and a quick sample can be [[https://www.zetetic.net/sqlcipher/open-source][here]], the source can be found [[https://github.com/sqlcipher/android-database-sqlcipher][here]]. - -*** Compatibility - - SQLCipher for Android runs on Android 2.1 - Android N, for =armeabi=, =armeabi-v7a=, =x86=, =x86_64=, and =arm64_v8a= architectures. - -*** Contributions - -We welcome contributions, to contribute to SQLCipher for Android, a [[https://www.zetetic.net/contributions/][contributor agreement]] needs to be submitted. All submissions should be based on the =master= branch. - -*** An Illustrative Terminal Listing - -A typical SQLite database in unencrypted, and visually parseable even as encoded text. The following example shows the difference between hexdumps of a standard SQLite db and one implementing SQLCipher. - -: ~ sjlombardo$ hexdump -C sqlite.db -: 00000000 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 |SQLite format 3.| -: … -: 000003c0 65 74 32 74 32 03 43 52 45 41 54 45 20 54 41 42 |et2t2.CREATE TAB| -: 000003d0 4c 45 20 74 32 28 61 2c 62 29 24 01 06 17 11 11 |LE t2(a,b)$…..| -: … -: 000007e0 20 74 68 65 20 73 68 6f 77 15 01 03 01 2f 01 6f | the show…./.o| -: 000007f0 6e 65 20 66 6f 72 20 74 68 65 20 6d 6f 6e 65 79 |ne for the money| -: -: ~ $ sqlite3 sqlcipher.db -: sqlite> PRAGMA KEY=’test123′; -: sqlite> CREATE TABLE t1(a,b); -: sqlite> INSERT INTO t1(a,b) VALUES (‘one for the money’, ‘two for the show’); -: sqlite> .quit -: -: ~ $ hexdump -C sqlcipher.db -: 00000000 84 d1 36 18 eb b5 82 90 c4 70 0d ee 43 cb 61 87 |.?6.?..?p.?C?a.| -: 00000010 91 42 3c cd 55 24 ab c6 c4 1d c6 67 b4 e3 96 bb |.B?..?| -: 00000bf0 8e 99 ee 28 23 43 ab a4 97 cd 63 42 8a 8e 7c c6 |..?(#C??.?cB..|?| -: -: ~ $ sqlite3 sqlcipher.db -: sqlite> SELECT * FROM t1; -: Error: file is encrypted or is not a database - -(example courtesy of SQLCipher) - -*** Details for Developers - -We’ve packaged up a very simple SDK for any Android developer to add SQLCipher into their app with the following three steps: - -1. Add a single sqlcipher.jar and a few .so’s to the application libs directory -2. Update the import path from =android.database.sqlite.*= to =net.sqlcipher.database.*= in any source files that reference it. The original =android.database.Cursor= can still be used unchanged. -3. Init the database in =onCreate()= and pass a variable argument to the open database method with a password: - -: SQLiteDatabase.loadLibs(this); //first init the db libraries with the context -: SQLiteOpenHelper.getWritableDatabase("thisismysecret"): - -An article covering both integration of SQLCipher into an Android application as well as building the source can be found [[http://sqlcipher.net/sqlcipher-for-android][here]]. - -Notepad + SQLCipher = Notepadbot - -Notepadbot is a sample application pulled from the standard Android samples code and updated to use SQLCipher. You can browse the source [[https://github.com/guardianproject/notepadbot][here]] and download the apk [[https://github.com/guardianproject/notepadbot/downloads][here]]. - -*** Building - -In order to build android-database-sqlcipher from source you will need both the Android SDK as well as Android NDK. With different Android SDK installation approaches available, please make sure the =android= binary is available on your =PATH=. We currently recommend using Android NDK version r11c. To complete the =make init= command, you will need the =android-23= platform installed from the SDK. The =make init= command will build OpenSSL for the required platforms, thus the =ANDROID_NDK_ROOT= environment variable must be defined which should point to your NDK root. Once you have cloned the repo, change directory into the root of the repository and run the following commands: - -: # this only needs to be done once -: make init - -: # to build the source -: make - -Recursively copy the =libs= directory into the root of your application, you will also need the =assets= directory copied into the root of your application folder. A detailed set of instructions and further customization can be found [[http://sqlcipher.net/sqlcipher-for-android/][here]]. - -*** License - -The Android support libraries are licensed under Apache 2.0, in line with the Android OS code on which they are based. The SQLCipher code itself is licensed under a BSD-style license from Zetetic LLC. Finally, the original SQLite code itself is in the public domain. diff --git a/SQLCIPHER_LICENSE b/SQLCIPHER_LICENSE index 21566c58..a8ae3b5e 100644 --- a/SQLCIPHER_LICENSE +++ b/SQLCIPHER_LICENSE @@ -2,7 +2,7 @@ http://sqlcipher.net Copyright (c) 2010 Zetetic LLC All rights reserved. - + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright @@ -13,7 +13,7 @@ http://sqlcipher.net * Neither the name of the ZETETIC LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - + THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE diff --git a/android-database-sqlcipher/build-openssl-libraries.sh b/android-database-sqlcipher/build-openssl-libraries.sh new file mode 100755 index 00000000..b1424cfa --- /dev/null +++ b/android-database-sqlcipher/build-openssl-libraries.sh @@ -0,0 +1,106 @@ +#! /usr/bin/env bash + +MINIMUM_ANDROID_SDK_VERSION=$1 +MINIMUM_ANDROID_64_BIT_SDK_VERSION=$2 +OPENSSL_DIR=$3 +ANDROID_LIB_ROOT=$4 + +(cd ${OPENSSL_DIR}; + + if [[ ! ${MINIMUM_ANDROID_SDK_VERSION} ]]; then + echo "MINIMUM_ANDROID_SDK_VERSION was not provided, include and rerun" + exit 1 + fi + + if [[ ! ${MINIMUM_ANDROID_64_BIT_SDK_VERSION} ]]; then + echo "MINIMUM_ANDROID_64_BIT_SDK_VERSION was not provided, include and rerun" + exit 1 + fi + + if [[ ! ${ANDROID_NDK_HOME} ]]; then + echo "ANDROID_NDK_HOME environment variable not set, set and rerun" + exit 1 + fi + + HOST_INFO=`uname -a` + case ${HOST_INFO} in + Darwin*) + TOOLCHAIN_SYSTEM=darwin-x86_64 + ;; + Linux*) + if [[ "${HOST_INFO}" == *i686* ]] + then + TOOLCHAIN_SYSTEM=linux-x86 + else + TOOLCHAIN_SYSTEM=linux-x86_64 + fi + ;; + *) + echo "Toolchain unknown for host system" + exit 1 + ;; + esac + + NDK_TOOLCHAIN_VERSION=4.9 + OPENSSL_CONFIGURE_OPTIONS="-fPIC -fstack-protector-all no-idea no-camellia \ + no-seed no-bf no-cast no-rc2 no-rc4 no-rc5 no-md2 \ + no-md4 no-ecdh no-sock no-ssl3 \ + no-dsa no-dh no-ec no-ecdsa no-tls1 \ + no-rfc3779 no-whirlpool no-srp \ + no-mdc2 no-ecdh no-engine \ + no-srtp" + + rm -rf ${ANDROID_LIB_ROOT} + + for SQLCIPHER_TARGET_PLATFORM in armeabi-v7a x86 x86_64 arm64-v8a + do + echo "Building libcrypto.a for ${SQLCIPHER_TARGET_PLATFORM}" + case "${SQLCIPHER_TARGET_PLATFORM}" in + armeabi-v7a) + CONFIGURE_ARCH="android-arm -march=armv7-a" + ANDROID_API_VERSION=${MINIMUM_ANDROID_SDK_VERSION} + OFFSET_BITS=32 + ;; + x86) + CONFIGURE_ARCH=android-x86 + ANDROID_API_VERSION=${MINIMUM_ANDROID_SDK_VERSION} + OFFSET_BITS=32 + ;; + x86_64) + CONFIGURE_ARCH=android64-x86_64 + ANDROID_API_VERSION=${MINIMUM_ANDROID_64_BIT_SDK_VERSION} + OFFSET_BITS=64 + ;; + arm64-v8a) + CONFIGURE_ARCH=android-arm64 + ANDROID_API_VERSION=${MINIMUM_ANDROID_64_BIT_SDK_VERSION} + OFFSET_BITS=64 + ;; + *) + echo "Unsupported build platform:${SQLCIPHER_TARGET_PLATFORM}" + exit 1 + esac + TOOLCHAIN_BIN_PATH=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${TOOLCHAIN_SYSTEM}/bin + PATH=${TOOLCHAIN_BIN_PATH}:${PATH} \ + ./Configure ${CONFIGURE_ARCH} \ + -D__ANDROID_API__=${ANDROID_API_VERSION} \ + -D_FILE_OFFSET_BITS=${OFFSET_BITS} \ + ${OPENSSL_CONFIGURE_OPTIONS} + + if [[ $? -ne 0 ]]; then + echo "Error executing:./Configure ${CONFIGURE_ARCH} ${OPENSSL_CONFIGURE_OPTIONS}" + exit 1 + fi + + make clean + PATH=${TOOLCHAIN_BIN_PATH}:${PATH} \ + make build_libs + + if [[ $? -ne 0 ]]; then + echo "Error executing make for platform:${SQLCIPHER_TARGET_PLATFORM}" + exit 1 + fi + mkdir -p ${ANDROID_LIB_ROOT}/${SQLCIPHER_TARGET_PLATFORM} + mv libcrypto.a ${ANDROID_LIB_ROOT}/${SQLCIPHER_TARGET_PLATFORM} + done +) diff --git a/android-database-sqlcipher/build.gradle b/android-database-sqlcipher/build.gradle new file mode 100644 index 00000000..38d6ab3b --- /dev/null +++ b/android-database-sqlcipher/build.gradle @@ -0,0 +1,58 @@ +apply plugin: "com.android.library" +apply plugin: "org.ec4j.editorconfig" +apply from: "native.gradle" +apply from: "maven.gradle" + +android { + + compileSdkVersion "${compileAndroidSdkVersion}" as Integer + + defaultConfig { + versionName "${clientVersionNumber}" + minSdkVersion "${minimumAndroidSdkVersion}" + targetSdkVersion "${targetAndroidSdkVersion}" + versionCode 1 + versionName "${clientVersionNumber}" + archivesBaseName = "${archivesBaseName}-${versionName}" + } + + editorconfig { + includes = ["src/**", "*.gradle"] + excludes = ["src/main/external/sqlcipher/**", "src/main/external/openssl-*/**"] + } + + buildTypes { + debug { + debuggable true + buildConfigField("String", "VERSION_NAME", "\"${clientVersionNumber}\"") + } + release { + debuggable false + minifyEnabled false + buildConfigField("String", "VERSION_NAME", "\"${clientVersionNumber}\"") + } + } + + sourceSets { + main { + jniLibs.srcDirs "${rootProject.ext.nativeRootOutputDir}/libs" + } + } + + dependencies { + implementation "androidx.sqlite:sqlite:2.2.0" + } + + editorconfig { + excludes = ['src/main/cpp/sqlite3.*', + 'src/main/external/sqlcipher/**', + 'src/main/external/openssl-*/**'] + } + + clean.dependsOn cleanNative + check.dependsOn editorconfigCheck + buildNative.mustRunAfter buildAmalgamation + buildAmalgamation.mustRunAfter buildOpenSSL + preBuild.dependsOn([buildOpenSSL, buildAmalgamation, copyAmalgamation, buildNative]) + buildNative.mustRunAfter(copyAmalgamation) +} diff --git a/android-database-sqlcipher/maven.gradle b/android-database-sqlcipher/maven.gradle new file mode 100644 index 00000000..59389bc7 --- /dev/null +++ b/android-database-sqlcipher/maven.gradle @@ -0,0 +1,99 @@ +apply plugin: "maven-publish" +apply plugin: "signing" +import org.gradle.plugins.signing.Sign + +def isReleaseBuild() { + return mavenVersionName.contains("SNAPSHOT") == false +} + +def getReleaseRepositoryUrl() { + return hasProperty('mavenReleaseRepositoryUrl') ? mavenReleaseRepositoryUrl + : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" +} + +def getSnapshotRepositoryUrl() { + if(hasProperty('mavenLocalRepositoryPrefix')) { + return "${mavenLocalRepositoryPrefix}${buildDir}/${mavenSnapshotRepositoryUrl}" + } else { + return hasProperty('mavenSnapshotRepositoryUrl') ? mavenSnapshotRepositoryUrl + : "https://oss.sonatype.org/content/repositories/snapshots/" + } +} + +def getRepositoryUsername() { + return hasProperty('nexusUsername') ? nexusUsername : "" +} + +def getRepositoryPassword() { + return hasProperty('nexusPassword') ? nexusPassword : "" +} + +gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.allTasks.any { it instanceof Sign }) { + allprojects { ext."signing.keyId" = "${signingKeyId}" } + allprojects { ext."signing.secretKeyRingFile" = "${signingKeyRingFile}" } + allprojects { ext."signing.password" = "${signingKeyPassword}" } + } +} + + + +afterEvaluate { project -> + publishing { + publications { + mavenJava(MavenPublication) { + from components.release + groupId = mavenGroup + artifactId = mavenArtifactId + version = mavenVersionName + pom { + name = mavenArtifactId + description = mavenPomDescription + url = mavenPomUrl + licenses { + license { + url = mavenLicenseUrl + } + } + developers { + developer { + name = mavenDeveloperName + email = mavenDeveloperEmail + } + } + scm { + connection = mavenScmConnection + developerConnection = mavenScmDeveloperConnection + url = mavenScmUrl + } + } + } + } + repositories { + maven { + def repoUrl = isReleaseBuild() + ? getReleaseRepositoryUrl() + : getSnapshotRepositoryUrl() + url = repoUrl + credentials { + username = getRepositoryUsername() + password = getRepositoryPassword() + } + } + } + } + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask("publish") } + sign publishing.publications.mavenJava + } + + task androidSourcesJar(type: Jar) { + classifier = "sources" + from android.sourceSets.main.java.sourceFiles + } + + artifacts { + archives androidSourcesJar + } +} diff --git a/android-database-sqlcipher/native.gradle b/android-database-sqlcipher/native.gradle new file mode 100644 index 00000000..8a04803b --- /dev/null +++ b/android-database-sqlcipher/native.gradle @@ -0,0 +1,170 @@ +import org.gradle.internal.logging.text.StyledTextOutputFactory +import static org.gradle.internal.logging.text.StyledTextOutput.Style + +task buildOpenSSL() { + onlyIf { + def armNativeFile = new File("${androidNativeRootDir}/armeabi-v7a/libcrypto.a") + if (armNativeFile.exists()) { + def out = services.get(StyledTextOutputFactory).create("") + out.style(Style.Normal).text("${androidNativeRootDir}/armeabi-v7a/libcrypto.a exists").style(Style.Info).println(' SKIPPED') + } + return !armNativeFile.exists() + } + doLast { + def nativeRootDirectory = new File("${androidNativeRootDir}") + if(!nativeRootDirectory.exists()){ + nativeRootDirectory.mkdirs() + } + exec { + workingDir "${projectDir}" + commandLine "./build-openssl-libraries.sh", + "${minimumAndroidSdkVersion}", + "${minimumAndroid64BitSdkVersion}", + "${opensslDir}", + "${androidNativeRootDir}" + } + } +} + +task buildAmalgamation() { + onlyIf { + def amalgamation = new File("${sqlcipherDir}/sqlite3.c") + return !amalgamation.exists() + } + doLast { + exec { + workingDir "${sqlcipherDir}" + environment("CFLAGS", "${sqlcipherCFlags}") + commandLine "./configure", "--enable-tempstore=yes", "--with-crypto-lib=none" + } + exec { + workingDir "${sqlcipherDir}" + environment("CFLAGS", "${sqlcipherCFlags}") + commandLine "make", "sqlite3.c" + } + } +} + +task copyAmalgamation() { + doLast { + exec { + workingDir "${sqlcipherDir}" + commandLine "cp", "sqlite3.c", "sqlite3.h", "${nativeRootOutputDir}/cpp/" + } + } +} + +task buildNative() { + description "Build the native SQLCipher binaries" + doLast { + executeNdkBuild( + "${nativeRootOutputDir}/libs32", + file("src/main/cpp").absolutePath, + file("src/main/cpp/Application32.mk").absolutePath, + "${sqlcipherCFlags}", "${otherSqlcipherCFlags}", + "${minimumAndroidSdkVersion}") + executeNdkBuild( + "${nativeRootOutputDir}/libs64", + file("src/main/cpp").absolutePath, + file("src/main/cpp/Application64.mk").absolutePath, + "${sqlcipherCFlags}", "${otherSqlcipherCFlags}", + "${minimumAndroid64BitSdkVersion}") + exec { + workingDir "${nativeRootOutputDir}" + commandLine "mkdir", "-p", "libs" + } + copy { + from fileTree("${nativeRootOutputDir}/libs32").include("*/*") + into "${nativeRootOutputDir}/libs" + from fileTree("${nativeRootOutputDir}/libs64").include("*/*") + into "${nativeRootOutputDir}/libs" + } + } +} + +task cleanOpenSSL() { + description "Clean the OpenSSL native libraries" + doLast { + logger.info "Cleaning OpenSSL native libraries" + exec { + workingDir "${androidNativeRootDir}" + ignoreExitValue true + commandLine "rm", "-rf" + } + } +} + +task cleanSQLCipher() { + description "Clean the SQLCipher source" + doLast { + exec { + workingDir "${sqlcipherDir}" + ignoreExitValue true + commandLine "make", "clean" + } + File amalgamationDestinationSource = new File("${nativeRootOutputDir}/cpp/sqlite3.c") + File amalgamationDestinationHeader = new File("${nativeRootOutputDir}/cpp/sqlite3.h") + if (amalgamationDestinationSource.exists()) amalgamationDestinationSource.delete() + if (amalgamationDestinationHeader.exists()) amalgamationDestinationHeader.delete() + } +} + +task cleanNative() { + description "Clean the native (JNI) build artifacts" + doLast { + logger.info "Cleaning native build artifacts" + ["libs", "libs32", "libs64", "obj"].each { + File file = new File("${projectDir}/src/main/${it}") + if (file.exists()) { + file.deleteDir() + } + } + } +} + +task distclean() { + description "Clean build, SQLCipher, and OpenSSL artifacts" + dependsOn clean, cleanSQLCipher, cleanOpenSSL + doLast { + new File("${androidNativeRootDir}/").deleteDir() + } +} + +def gitClean(directory) { + logger.info "Cleaning directory:${directory}" + exec { + workingDir "${directory}" + commandLine "git", "checkout", "-f" + } + exec { + workingDir "${directory}" + commandLine "git", "clean", "-d", "-f" + } +} + +def executeNdkBuild(outputDir, androidMkDirectory, applicationMkFile, + cflags, otherSqlcipherCFlags, androidVersion) { + logger.info "Executing NDK build command" + def out = services.get(StyledTextOutputFactory).create("") + out.style(Style.Normal).text("SQLCIPHER_CFLAGS=").style(Style.Info).println("${cflags}") + out.style(Style.Normal).text("OPENSSL_DIR=").style(Style.Info).println("${opensslDir}") + out.style(Style.Normal).text("SQLCIPHER_DIR=").style(Style.Info).println("${sqlcipherDir}") + out.style(Style.Normal).text("SQLCIPHER_OTHER_CFLAGS=").style(Style.Info).println("${otherSqlcipherCFlags}") + out.style(Style.Normal).text("ANDROID_NATIVE_ROOT_DIR=").style(Style.Info).println("${androidNativeRootDir}") + out.style(Style.Normal).text("NDK_APP_PLATFORM=").style(Style.Info).println("${androidVersion}") + + exec { + def outputDirectory = "NDK_LIBS_OUT=${outputDir}" + def applicationFile = "NDK_APPLICATION_MK=${applicationMkFile}" + def environmentVariables = ["SQLCIPHER_CFLAGS" : "${cflags}", + "OPENSSL_DIR" : "${opensslDir}", + "SQLCIPHER_DIR" : "${sqlcipherDir}", + "SQLCIPHER_OTHER_CFLAGS" : "${otherSqlcipherCFlags}", + "ANDROID_NATIVE_ROOT_DIR": "${androidNativeRootDir}", + "NDK_APP_PLATFORM" : "${androidVersion}"] + environment(environmentVariables) + commandLine "ndk-build", "V=1", "${ndkBuildType}", + "--environment-overrides", outputDirectory, + "-C", androidMkDirectory, applicationFile + } +} diff --git a/android-database-sqlcipher/src/main/AndroidManifest.xml b/android-database-sqlcipher/src/main/AndroidManifest.xml new file mode 100644 index 00000000..463aa005 --- /dev/null +++ b/android-database-sqlcipher/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/src/net/sqlcipher/IContentObserver.aidl b/android-database-sqlcipher/src/main/aidl/net/sqlcipher/IContentObserver.aidl similarity index 85% rename from src/net/sqlcipher/IContentObserver.aidl rename to android-database-sqlcipher/src/main/aidl/net/sqlcipher/IContentObserver.aidl index b534a0e4..22857515 100755 --- a/src/net/sqlcipher/IContentObserver.aidl +++ b/android-database-sqlcipher/src/main/aidl/net/sqlcipher/IContentObserver.aidl @@ -2,16 +2,16 @@ ** ** Copyright 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 +** 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 +** 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 +** 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. */ diff --git a/jni/Android.mk b/android-database-sqlcipher/src/main/cpp/Android.mk similarity index 54% rename from jni/Android.mk rename to android-database-sqlcipher/src/main/cpp/Android.mk index 831e08f5..0564b775 100644 --- a/jni/Android.mk +++ b/android-database-sqlcipher/src/main/cpp/Android.mk @@ -2,16 +2,14 @@ LOCAL_PATH := $(call my-dir) MY_PATH := $(LOCAL_PATH) include $(CLEAR_VARS) LOCAL_PATH := $(MY_PATH) -SQLCIPHER_DIR := $(LOCAL_PATH)/../external/sqlcipher -SQLCIPHER_SRC := $(SQLCIPHER_DIR)/sqlite3.c -LOCAL_CFLAGS += $(SQLCIPHER_CFLAGS) -DLOG_NDEBUG -LOCAL_C_INCLUDES := $(SQLCIPHER_DIR) $(LOCAL_PATH) -LOCAL_LDLIBS := -llog -latomic -LOCAL_LDFLAGS += -L$(LOCAL_PATH)/android-libs/$(TARGET_ARCH_ABI) -fuse-ld=bfd +LOCAL_CFLAGS += $(SQLCIPHER_CFLAGS) $(SQLCIPHER_OTHER_CFLAGS) +LOCAL_C_INCLUDES += $(LOCAL_PATH) +LOCAL_LDLIBS := -llog +LOCAL_LDFLAGS += -L$(ANDROID_NATIVE_ROOT_DIR)/$(TARGET_ARCH_ABI) LOCAL_STATIC_LIBRARIES += static-libcrypto LOCAL_MODULE := libsqlcipher -LOCAL_SRC_FILES := $(SQLCIPHER_SRC) \ +LOCAL_SRC_FILES := sqlite3.c \ jni_exception.cpp \ net_sqlcipher_database_SQLiteCompiledSql.cpp \ net_sqlcipher_database_SQLiteDatabase.cpp \ @@ -25,6 +23,6 @@ include $(BUILD_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := static-libcrypto -LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../external/openssl/include -LOCAL_SRC_FILES := $(LOCAL_PATH)/../external/android-libs/$(TARGET_ARCH_ABI)/libcrypto.a +LOCAL_EXPORT_C_INCLUDES := $(OPENSSL_DIR)/include +LOCAL_SRC_FILES := $(ANDROID_NATIVE_ROOT_DIR)/$(TARGET_ARCH_ABI)/libcrypto.a include $(PREBUILT_STATIC_LIBRARY) diff --git a/android-database-sqlcipher/src/main/cpp/Application32.mk b/android-database-sqlcipher/src/main/cpp/Application32.mk new file mode 100644 index 00000000..8d24d563 --- /dev/null +++ b/android-database-sqlcipher/src/main/cpp/Application32.mk @@ -0,0 +1,7 @@ +APP_PROJECT_PATH := $(shell pwd) +APP_ABI := armeabi-v7a x86 +APP_PLATFORM := android-$(NDK_APP_PLATFORM) +APP_BUILD_SCRIPT := $(APP_PROJECT_PATH)/Android.mk +APP_STL := c++_static +APP_CFLAGS := -D_FILE_OFFSET_BITS=32 +APP_LDFLAGS += -Wl,--exclude-libs,ALL diff --git a/android-database-sqlcipher/src/main/cpp/Application64.mk b/android-database-sqlcipher/src/main/cpp/Application64.mk new file mode 100644 index 00000000..a4d604b4 --- /dev/null +++ b/android-database-sqlcipher/src/main/cpp/Application64.mk @@ -0,0 +1,7 @@ +APP_PROJECT_PATH := $(shell pwd) +APP_ABI := x86_64 arm64-v8a +APP_PLATFORM := android-$(NDK_APP_PLATFORM) +APP_BUILD_SCRIPT := $(APP_PROJECT_PATH)/Android.mk +APP_STL := c++_static +APP_CFLAGS := -D_FILE_OFFSET_BITS=64 +APP_LDFLAGS += -Wl,--exclude-libs,ALL diff --git a/jni/CursorWindow.cpp b/android-database-sqlcipher/src/main/cpp/CursorWindow.cpp similarity index 75% rename from jni/CursorWindow.cpp rename to android-database-sqlcipher/src/main/cpp/CursorWindow.cpp index 7a8bac6a..e58fb3c5 100644 --- a/jni/CursorWindow.cpp +++ b/android-database-sqlcipher/src/main/cpp/CursorWindow.cpp @@ -1,71 +1,50 @@ /* * 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 + * 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 + * 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 + * 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. */ #undef LOG_TAG #define LOG_TAG "CursorWindow" -// #include -// #include -// #include - #include #include #include #include - #include -// #include - #include "CursorWindow.h" namespace sqlcipher { -CursorWindow::CursorWindow(size_t maxSize) : - mMaxSize(maxSize) +CursorWindow::CursorWindow(size_t initialSize, size_t growthPaddingSize, size_t maxSize) { + mInitialSize = initialSize; + mGrowthPaddingSize = growthPaddingSize; + mMaxSize = maxSize; + LOG_WINDOW("CursorWindow::CursorWindow initialSize:%d growBySize:%d maxSize:%d\n", + initialSize, growthPaddingSize, maxSize); } -// bool CursorWindow::setMemory(const android::sp& memory) -// { -// mMemory = memory; -// mData = (uint8_t *) memory->pointer(); -// if (mData == NULL) { -// return false; - -// } -// mHeader = (window_header_t *) mData; - -// // Make the window read-only -// ssize_t size = memory->size(); -// mSize = size; -// mMaxSize = size; -// mFreeOffset = size; -// LOG_WINDOW("Created CursorWindow from existing IMemory: mFreeOffset = %d, numRows = %d, numColumns = %d, mSize = %d, mMaxSize = %d, mData = %p", mFreeOffset, mHeader->numRows, mHeader->numColumns, mSize, mMaxSize, mData); -// return true; -// } - bool CursorWindow::initBuffer(bool localOnly) { - void* data = malloc(mMaxSize); + void* data = malloc(mInitialSize); if(data){ mData = (uint8_t *) data; mHeader = (window_header_t *) mData; - mSize = mMaxSize; + mSize = mInitialSize; clear(); - LOG_WINDOW("Created CursorWindow with new MemoryDealer: mFreeOffset = %d, mSize = %d, mMaxSize = %d, mData = %p", mFreeOffset, mSize, mMaxSize, mData); + LOG_WINDOW("Created CursorWindow with new MemoryDealer: mFreeOffset = %d, mSize = %d, mInitialSize = %d, mGrowthPaddingSize = %d, mMaxSize = %d, mData = %p\n", + mFreeOffset, mSize, mInitialSize, mGrowthPaddingSize, mMaxSize, mData); return true; } return false; @@ -85,6 +64,8 @@ void CursorWindow::clear() mFreeOffset = sizeof(window_header_t) + ROW_SLOT_CHUNK_SIZE; // Mark the first chunk's next 'pointer' as null *((uint32_t *)(mData + mFreeOffset - sizeof(uint32_t))) = 0; + mChunkNumToNextChunkOffset.clear(); + mLastChunkPtrOffset = 0; } int32_t CursorWindow::freeSpace() @@ -122,7 +103,7 @@ field_slot_t * CursorWindow::allocRow() // If the last alloc relocated mData this will be rowSlot's new address, otherwise the value will not change rowSlot = (row_slot_t*)(mData + rowSlotOffset); -LOG_WINDOW("Allocated row %u, rowSlot is at offset %u, fieldDir is %d bytes at offset %u\n", (mHeader->numRows - 1), ((uint8_t *)rowSlot) - mData, fieldDirSize, fieldDirOffset); + LOG_WINDOW("Allocated row %u, rowSlot is at offset %u, fieldDir is %d bytes at offset %u\n", (mHeader->numRows - 1), ((uint8_t *)rowSlot) - mData, fieldDirSize, fieldDirOffset); rowSlot->offset = fieldDirOffset; return fieldDir; @@ -141,15 +122,19 @@ uint32_t CursorWindow::alloc(size_t requestedSize, bool aligned) } size = requestedSize + padding; if (size > freeSpace()) { - LOGE("need to grow: mSize = %d, size = %d, freeSpace() = %d, numRows = %d", - mSize, size, freeSpace(), mHeader->numRows); - new_allocation_sz = mSize + size - freeSpace() + GROW_WINDOW_SIZE_EXTRA; + new_allocation_sz = mSize + size - freeSpace() + mGrowthPaddingSize; + LOGE("need to grow: mSize = %d, size = %d, freeSpace() = %d, numRows = %d new_allocation_sz:%d\n", + mSize, size, freeSpace(), mHeader->numRows, new_allocation_sz); + if(mMaxSize == 0 || new_allocation_sz <= mMaxSize) { tempData = realloc((void *)mData, new_allocation_sz); if(tempData == NULL) return 0; mData = (uint8_t *)tempData; mHeader = (window_header_t *)mData; LOGE("allocation grew to:%d", new_allocation_sz); mSize = new_allocation_sz; + } else { + return 0; + } } uint32_t offset = mFreeOffset + padding; mFreeOffset += size; @@ -158,17 +143,29 @@ uint32_t CursorWindow::alloc(size_t requestedSize, bool aligned) row_slot_t * CursorWindow::getRowSlot(int row) { - LOG_WINDOW("enter getRowSlot current row num %d, this row %d", mHeader->numRows, row); + LOG_WINDOW("getRowSlot entered: requesting row:%d, current row num:%d", row, mHeader->numRows); + unordered_map::iterator result; int chunkNum = row / ROW_SLOT_CHUNK_NUM_ROWS; int chunkPos = row % ROW_SLOT_CHUNK_NUM_ROWS; int chunkPtrOffset = sizeof(window_header_t) + ROW_SLOT_CHUNK_SIZE - sizeof(uint32_t); uint8_t * rowChunk = mData + sizeof(window_header_t); + + // check for chunkNum in cache + result = mChunkNumToNextChunkOffset.find(chunkNum); + if(result != mChunkNumToNextChunkOffset.end()){ + rowChunk = offsetToPtr(result->second); + LOG_WINDOW("Retrieved chunk offset from cache for row:%d", row); + return (row_slot_t *)(rowChunk + (chunkPos * sizeof(row_slot_t))); + } + + // walk the list, this shouldn't occur + LOG_WINDOW("getRowSlot walking list %d times to find rowslot for row:%d", chunkNum, row); for (int i = 0; i < chunkNum; i++) { rowChunk = offsetToPtr(*((uint32_t *)(mData + chunkPtrOffset))); chunkPtrOffset = rowChunk - mData + (ROW_SLOT_CHUNK_NUM_ROWS * sizeof(row_slot_t)); } return (row_slot_t *)(rowChunk + (chunkPos * sizeof(row_slot_t))); - LOG_WINDOW("exit getRowSlot current row num %d, this row %d", mHeader->numRows, row); + LOG_WINDOW("exit getRowSlot current row num %d, this row %d", mHeader->numRows, row); } row_slot_t * CursorWindow::allocRowSlot() @@ -177,38 +174,49 @@ row_slot_t * CursorWindow::allocRowSlot() int chunkPos = mHeader->numRows % ROW_SLOT_CHUNK_NUM_ROWS; int chunkPtrOffset = sizeof(window_header_t) + ROW_SLOT_CHUNK_SIZE - sizeof(uint32_t); uint8_t * rowChunk = mData + sizeof(window_header_t); -LOG_WINDOW("Allocating row slot, mHeader->numRows is %d, chunkNum is %d, chunkPos is %d", mHeader->numRows, chunkNum, chunkPos); - for (int i = 0; i < chunkNum; i++) { + LOG_WINDOW("allocRowSlot entered: Allocating row slot, mHeader->numRows is %d, chunkNum is %d, chunkPos is %d", + mHeader->numRows, chunkNum, chunkPos); + + if(mLastChunkPtrOffset != 0){ + chunkPtrOffset = mLastChunkPtrOffset; + } + if(chunkNum > 0) { uint32_t nextChunkOffset = *((uint32_t *)(mData + chunkPtrOffset)); -LOG_WINDOW("nextChunkOffset is %d", nextChunkOffset); + LOG_WINDOW("nextChunkOffset is %d", nextChunkOffset); if (nextChunkOffset == 0) { + mLastChunkPtrOffset = chunkPtrOffset; // Allocate a new row chunk nextChunkOffset = alloc(ROW_SLOT_CHUNK_SIZE, true); + mChunkNumToNextChunkOffset.insert(make_pair(chunkNum, nextChunkOffset)); if (nextChunkOffset == 0) { return NULL; } rowChunk = offsetToPtr(nextChunkOffset); -LOG_WINDOW("allocated new chunk at %d, rowChunk = %p", nextChunkOffset, rowChunk); + LOG_WINDOW("allocated new chunk at %d, rowChunk = %p", nextChunkOffset, rowChunk); *((uint32_t *)(mData + chunkPtrOffset)) = rowChunk - mData; // Mark the new chunk's next 'pointer' as null *((uint32_t *)(rowChunk + ROW_SLOT_CHUNK_SIZE - sizeof(uint32_t))) = 0; } else { -LOG_WINDOW("follwing 'pointer' to next chunk, offset of next pointer is %d", chunkPtrOffset); + LOG_WINDOW("follwing 'pointer' to next chunk, offset of next pointer is %d", chunkPtrOffset); rowChunk = offsetToPtr(nextChunkOffset); chunkPtrOffset = rowChunk - mData + (ROW_SLOT_CHUNK_NUM_ROWS * sizeof(row_slot_t)); + if(chunkPos == ROW_SLOT_CHUNK_NUM_ROWS - 1){ + // prepare to allocate new rowslot_t now at end of row + mLastChunkPtrOffset = chunkPtrOffset; + } } } mHeader->numRows++; - return (row_slot_t *)(rowChunk + (chunkPos * sizeof(row_slot_t))); } field_slot_t * CursorWindow::getFieldSlotWithCheck(int row, int column) { + LOG_WINDOW("getFieldSlotWithCheck entered: row:%d column:%d", row, column); if (row < 0 || row >= mHeader->numRows || column < 0 || column >= mHeader->numColumns) { LOGE("Bad request for field slot %d,%d. numRows = %d, numColumns = %d", row, column, mHeader->numRows, mHeader->numColumns); return NULL; - } + } row_slot_t * rowSlot = getRowSlot(row); if (!rowSlot) { LOGE("Failed to find rowSlot for row %d", row); @@ -217,17 +225,18 @@ field_slot_t * CursorWindow::getFieldSlotWithCheck(int row, int column) if (rowSlot->offset == 0 || rowSlot->offset >= mSize) { LOGE("Invalid rowSlot, offset = %d", rowSlot->offset); return NULL; - } + } int fieldDirOffset = rowSlot->offset; - return ((field_slot_t *)offsetToPtr(fieldDirOffset)) + column; + return ((field_slot_t *)offsetToPtr(fieldDirOffset)) + column; } uint32_t CursorWindow::read_field_slot(int row, int column, field_slot_t * slotOut) { + LOG_WINDOW("read_field_slot entered: row:%d, column:%d, slotOut:%p", row, column, slotOut); if (row < 0 || row >= mHeader->numRows || column < 0 || column >= mHeader->numColumns) { LOGE("Bad request for field slot %d,%d. numRows = %d, numColumns = %d", row, column, mHeader->numRows, mHeader->numColumns); return -1; - } + } row_slot_t * rowSlot = getRowSlot(row); if (!rowSlot) { LOGE("Failed to find rowSlot for row %d", row); @@ -237,9 +246,9 @@ uint32_t CursorWindow::read_field_slot(int row, int column, field_slot_t * slotO LOGE("Invalid rowSlot, offset = %d", rowSlot->offset); return -1; } -LOG_WINDOW("Found field directory for %d,%d at rowSlot %d, offset %d", row, column, (uint8_t *)rowSlot - mData, rowSlot->offset); + LOG_WINDOW("Found field directory for %d,%d at rowSlot %d, offset %d", row, column, (uint8_t *)rowSlot - mData, rowSlot->offset); field_slot_t * fieldDir = (field_slot_t *)offsetToPtr(rowSlot->offset); -LOG_WINDOW("Read field_slot_t %d,%d: offset = %d, size = %d, type = %d", row, column, fieldDir[column].data.buffer.offset, fieldDir[column].data.buffer.size, fieldDir[column].type); + LOG_WINDOW("Read field_slot_t %d,%d: offset = %d, size = %d, type = %d", row, column, fieldDir[column].data.buffer.offset, fieldDir[column].data.buffer.size, fieldDir[column].type); // Copy the data to the out param slotOut->data.buffer.offset = fieldDir[column].data.buffer.offset; @@ -250,7 +259,7 @@ LOG_WINDOW("Read field_slot_t %d,%d: offset = %d, size = %d, type = %d", row, co void CursorWindow::copyIn(uint32_t offset, uint8_t const * data, size_t size) { - assert(offset + size <= mSize); + assert(offset + size <= mSize); memcpy(mData + offset, data, size); } @@ -355,7 +364,7 @@ bool CursorWindow::getLong(unsigned int row, unsigned int col, int64_t * valueOu if (!fieldSlot || fieldSlot->type != FIELD_TYPE_INTEGER) { return false; } - + #if WINDOW_STORAGE_INLINE_NUMERICS *valueOut = fieldSlot->data.l; #else @@ -385,7 +394,7 @@ bool CursorWindow::getNull(unsigned int row, unsigned int col, bool * valueOut) if (!fieldSlot) { return false; } - + if (fieldSlot->type != FIELD_TYPE_NULL) { *valueOut = false; } else { diff --git a/jni/CursorWindow.h b/android-database-sqlcipher/src/main/cpp/CursorWindow.h similarity index 89% rename from jni/CursorWindow.h rename to android-database-sqlcipher/src/main/cpp/CursorWindow.h index 4060ac87..9ad5cfb8 100644 --- a/jni/CursorWindow.h +++ b/android-database-sqlcipher/src/main/cpp/CursorWindow.h @@ -1,16 +1,16 @@ /* * 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 + * 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 + * 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 + * 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. */ @@ -24,24 +24,17 @@ #include #include #include "log.h" +#include -// #include -// #include -// #include - -#define DEFAULT_WINDOW_SIZE 4096 -#define MAX_WINDOW_SIZE (1024 * 1024) -#define WINDOW_ALLOCATION_SIZE 4096 - -#define ROW_SLOT_CHUNK_NUM_ROWS 16 - -#define GROW_WINDOW_SIZE_EXTRA MAX_WINDOW_SIZE / 10 +#define ROW_SLOT_CHUNK_NUM_ROWS 128 +#define INITIAL_WINDOW_SIZE (1024 * 1024) +#define GROW_WINDOW_SIZE_EXTRA INITIAL_WINDOW_SIZE +#define WINDOW_ALLOCATION_UNBOUNDED 0 // Row slots are allocated in chunks of ROW_SLOT_CHUNK_NUM_ROWS, // with an offset after the rows that points to the next chunk #define ROW_SLOT_CHUNK_SIZE ((ROW_SLOT_CHUNK_NUM_ROWS * sizeof(row_slot_t)) + sizeof(uint32_t)) - #if LOG_NDEBUG #define IF_LOG_WINDOW() if (false) @@ -54,13 +47,16 @@ #endif - // When defined to true strings are stored as UTF8, otherwise they're UTF16 #define WINDOW_STORAGE_UTF8 0 -// When defined to true numberic values are stored inline in the field_slot_t, otherwise they're allocated in the window +// When defined to true numberic values are stored inline in the field_slot_t, +// otherwise they're allocated in the window #define WINDOW_STORAGE_INLINE_NUMERICS 1 +using std::make_pair; +using std::unordered_map; + namespace sqlcipher { typedef struct @@ -104,14 +100,11 @@ typedef struct class CursorWindow { public: - CursorWindow(size_t maxSize); + CursorWindow(size_t initialSize, size_t growthPaddingSize, size_t maxSize); CursorWindow(){} - /* bool setMemory(const android::sp&); */ ~CursorWindow(); bool initBuffer(bool localOnly); - /* android::sp getMemory() {return mMemory;} */ - size_t size() {return mSize;} uint8_t * data() {return mData;} uint32_t getNumRows() {return mHeader->numRows;} @@ -177,7 +170,7 @@ class CursorWindow row_slot_t * allocRowSlot(); row_slot_t * getRowSlot(int row); - + /** * return NULL if Failed to find rowSlot or * Invalid rowSlot @@ -192,14 +185,16 @@ class CursorWindow private: uint8_t * mData; size_t mSize; + size_t mInitialSize; + size_t mGrowthPaddingSize; size_t mMaxSize; window_header_t * mHeader; - /* android::sp mMemory; */ - /** * Offset of the lowest unused data byte in the array. */ uint32_t mFreeOffset; + unordered_map mChunkNumToNextChunkOffset; + int mLastChunkPtrOffset; }; }; // namespace sqlcipher diff --git a/jni/jni_elements.h b/android-database-sqlcipher/src/main/cpp/jni_elements.h similarity index 100% rename from jni/jni_elements.h rename to android-database-sqlcipher/src/main/cpp/jni_elements.h diff --git a/jni/jni_exception.cpp b/android-database-sqlcipher/src/main/cpp/jni_exception.cpp similarity index 92% rename from jni/jni_exception.cpp rename to android-database-sqlcipher/src/main/cpp/jni_exception.cpp index 41a91ff8..142d606f 100644 --- a/jni/jni_exception.cpp +++ b/android-database-sqlcipher/src/main/cpp/jni_exception.cpp @@ -1,4 +1,3 @@ -#include #include "jni_exception.h" void jniThrowException(JNIEnv* env, const char* exceptionClass, const char* sqlite3Message) { diff --git a/jni/jni_exception.h b/android-database-sqlcipher/src/main/cpp/jni_exception.h similarity index 100% rename from jni/jni_exception.h rename to android-database-sqlcipher/src/main/cpp/jni_exception.h diff --git a/jni/log.h b/android-database-sqlcipher/src/main/cpp/log.h similarity index 100% rename from jni/log.h rename to android-database-sqlcipher/src/main/cpp/log.h diff --git a/jni/net_sqlcipher_CursorWindow.cpp b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_CursorWindow.cpp similarity index 97% rename from jni/net_sqlcipher_CursorWindow.cpp rename to android-database-sqlcipher/src/main/cpp/net_sqlcipher_CursorWindow.cpp index 0c1d11cd..b34b1355 100644 --- a/jni/net_sqlcipher_CursorWindow.cpp +++ b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_CursorWindow.cpp @@ -50,13 +50,15 @@ namespace sqlcipher { return GET_WINDOW(env, javaWindow); } - static void native_init_empty(JNIEnv * env, jobject object, jboolean localOnly) + static void native_init_empty(JNIEnv * env, jobject object, + jboolean localOnly, jlong initialSize, + jlong growthPaddingSize, jlong maxSize) { uint8_t * data; size_t size; CursorWindow * window; - window = new CursorWindow(MAX_WINDOW_SIZE); + window = new CursorWindow(initialSize, growthPaddingSize, maxSize); if (!window) { jniThrowException(env, "java/lang/RuntimeException", "No memory for native window object"); return; @@ -96,14 +98,14 @@ namespace sqlcipher { { char buf[100]; snprintf(buf, sizeof(buf), "get field slot from row %d col %d failed", row, column); - jniThrowException(env, "java/lang/IllegalStateException", buf); + jniThrowException(env, "net/sqlcipher/InvalidRowColumnException", buf); } static void throwUnknowTypeException(JNIEnv * env, jint type) { char buf[80]; snprintf(buf, sizeof(buf), "UNKNOWN type %d", type); - jniThrowException(env, "java/lang/IllegalStateException", buf); + jniThrowException(env, "net/sqlcipher/UnknownTypeException", buf); } static jlong getLong_native(JNIEnv * env, jobject object, jint row, jint column) @@ -283,7 +285,7 @@ namespace sqlcipher { int64_t value; if (window->getLong(row, column, &value)) { char buf[32]; - snprintf(buf, sizeof(buf), "%lld", value); + snprintf(buf, sizeof(buf), "%" PRId64 "", value); return env->NewStringUTF((const char*)buf); } return NULL; @@ -294,8 +296,8 @@ namespace sqlcipher { snprintf(buf, sizeof(buf), "%g", value); return env->NewStringUTF(buf); } - return NULL; } + return NULL; } /** @@ -359,7 +361,7 @@ namespace sqlcipher { if (window->getLong(row, column, &value)) { int len; char buf[32]; - len = snprintf(buf, sizeof(buf), "%lld", value); + len = snprintf(buf, sizeof(buf), "%" PRId64 "", value); jint bufferLength = env->GetArrayLength(buffer); if(len > bufferLength || dst == NULL){ jstring content = env->NewStringUTF(buf); @@ -616,7 +618,7 @@ namespace sqlcipher { static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ - {"native_init", "(Z)V", (void *)native_init_empty}, + {"native_init", "(ZJJJ)V", (void *)native_init_empty}, // {"native_init", "(Landroid/os/IBinder;)V", (void *)native_init_memory}, // {"native_getBinder", "()Landroid/os/IBinder;", (void *)native_getBinder}, {"native_clear", "()V", (void *)native_clear}, diff --git a/jni/net_sqlcipher_database_SQLiteCompiledSql.cpp b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteCompiledSql.cpp similarity index 99% rename from jni/net_sqlcipher_database_SQLiteCompiledSql.cpp rename to android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteCompiledSql.cpp index 0bae80cb..4db2864e 100644 --- a/jni/net_sqlcipher_database_SQLiteCompiledSql.cpp +++ b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteCompiledSql.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include "log.h" diff --git a/jni/net_sqlcipher_database_SQLiteDatabase.cpp b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteDatabase.cpp similarity index 95% rename from jni/net_sqlcipher_database_SQLiteDatabase.cpp rename to android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteDatabase.cpp index fd7b8bd7..503372ad 100644 --- a/jni/net_sqlcipher_database_SQLiteDatabase.cpp +++ b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteDatabase.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -151,7 +152,7 @@ namespace sqlcipher { env->ReleaseCharArrayElements(jKey, jKeyChar, JNI_ABORT); env->ReleaseStringUTFChars(key, password); } - + void native_rawExecSQL(JNIEnv* env, jobject object, jstring sql) { sqlite3 * handle = (sqlite3 *)env->GetLongField(object, offset_db_handle); @@ -601,7 +602,7 @@ namespace sqlcipher { concatenated with the given message */ void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle, const char* message) { - if (handle) { + if (handle && sqlite3_errcode(handle) != SQLITE_OK) { throw_sqlite3_exception(env, sqlite3_errcode(handle), sqlite3_errmsg(handle), message); } else { @@ -622,6 +623,20 @@ namespace sqlcipher { } } + void throw_sqlite3_exception_errcode(JNIEnv* env, + int errcode, + int extended_err_code, + const char* message) { + if (errcode == SQLITE_DONE) { + throw_sqlite3_exception(env, errcode, NULL, message); + } else { + char temp[55]; + sprintf(temp, "error code %d (extended error code %d)", + errcode, extended_err_code); + throw_sqlite3_exception(env, errcode, temp, message); + } + } + /* throw a SQLiteException for a given error code, sqlite3message, and user message */ @@ -630,28 +645,28 @@ namespace sqlcipher { const char* exceptionClass; switch (errcode) { case SQLITE_IOERR: - exceptionClass = "net/sqlcipher/database/SQLiteDiskIOException"; + exceptionClass = "android/database/sqlite/SQLiteDiskIOException"; break; case SQLITE_CORRUPT: - exceptionClass = "net/sqlcipher/database/SQLiteDatabaseCorruptException"; + exceptionClass = "android/database/sqlite/SQLiteDatabaseCorruptException"; break; case SQLITE_CONSTRAINT: - exceptionClass = "net/sqlcipher/database/SQLiteConstraintException"; + exceptionClass = "android/database/sqlite/SQLiteConstraintException"; break; case SQLITE_ABORT: - exceptionClass = "net/sqlcipher/database/SQLiteAbortException"; + exceptionClass = "android/database/sqlite/SQLiteAbortException"; break; case SQLITE_DONE: - exceptionClass = "net/sqlcipher/database/SQLiteDoneException"; + exceptionClass = "android/database/sqlite/SQLiteDoneException"; break; case SQLITE_FULL: - exceptionClass = "net/sqlcipher/database/SQLiteFullException"; + exceptionClass = "android/database/sqlite/SQLiteFullException"; break; case SQLITE_MISUSE: - exceptionClass = "net/sqlcipher/database/SQLiteMisuseException"; + exceptionClass = "android/database/sqlite/SQLiteMisuseException"; break; default: - exceptionClass = "net/sqlcipher/database/SQLiteException"; + exceptionClass = "android/database/sqlite/SQLiteException"; break; } diff --git a/jni/net_sqlcipher_database_SQLiteDebug.cpp b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteDebug.cpp similarity index 99% rename from jni/net_sqlcipher_database_SQLiteDebug.cpp rename to android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteDebug.cpp index 130345a3..15a31cf6 100644 --- a/jni/net_sqlcipher_database_SQLiteDebug.cpp +++ b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteDebug.cpp @@ -108,7 +108,7 @@ static int read_mapinfo(FILE *fp, again: skip = 0; - + if(fgets(line, 1024, fp) == 0) return 0; len = strlen(line); @@ -138,7 +138,7 @@ static int read_mapinfo(FILE *fp, if (sscanf(line, "Private_Dirty: %d kB", &private_dirty) != 1) return 0; if (fgets(line, 1024, fp) == 0) return 0; if (sscanf(line, "Referenced: %d kB", &referenced) != 1) return 0; - + if (skip) { goto again; } @@ -154,11 +154,11 @@ static void load_maps(int pid, int *sharedPages, int *privatePages) { char tmp[128]; FILE *fp; - + sprintf(tmp, "/proc/%d/smaps", pid); fp = fopen(tmp, "r"); if (fp == 0) return; - + while (read_mapinfo(fp, sharedPages, privatePages) != 0) { // Do nothing } diff --git a/jni/net_sqlcipher_database_SQLiteProgram.cpp b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteProgram.cpp similarity index 100% rename from jni/net_sqlcipher_database_SQLiteProgram.cpp rename to android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteProgram.cpp diff --git a/jni/net_sqlcipher_database_SQLiteQuery.cpp b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteQuery.cpp similarity index 83% rename from jni/net_sqlcipher_database_SQLiteQuery.cpp rename to android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteQuery.cpp index e10ebf7e..8dcf139f 100644 --- a/jni/net_sqlcipher_database_SQLiteQuery.cpp +++ b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteQuery.cpp @@ -104,7 +104,8 @@ static int finish_program_and_get_row_count(sqlite3_stmt *statement) { } static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, - jint startPos, jint offsetParam, jint maxRead, jint lastPos) + jint startPos, jint requiredPos, + jint offsetParam, jint maxRead, jint lastPos) { int err; sqlite3_stmt * statement = GET_STATEMENT(env, object); @@ -114,7 +115,7 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, int retryCount; int boundParams; CursorWindow * window; - + if (statement == NULL) { LOGE("Invalid statement in fillWindow()"); jniThrowException(env, "java/lang/IllegalStateException", @@ -165,8 +166,8 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, LOGE("startPos %d > actual rows %d", startPos, num); return num; } - } - + } + while(startPos != 0 || numRows < maxRead) { err = sqlite3_step(statement); if (err == SQLITE_ROW) { @@ -178,6 +179,13 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, // the field data is being allocated. { field_slot_t * fieldDir = window->allocRow(); + if(!fieldDir && (startPos + numRows) < requiredPos) { + LOG_WINDOW("Failed to allocate row, resetting window", startPos + numRows); + window->clear(); + window->setNumColumns(numColumns); + fieldDir = window->allocRow(); + LOG_WINDOW("Window reset, row allocated at %p", fieldDir); + } if (!fieldDir) { LOGE("Failed allocating fieldDir at startPos %d row %d", startPos, numRows); return startPos + numRows + finish_program_and_get_row_count(statement) + 1; @@ -186,7 +194,36 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, // Pack the row into the window int i; + bool failed = false; + bool reset = false; for (i = 0; i < numColumns; i++) { + + if(reset) { + LOG_WINDOW("Reset requested for row %d, likely cursor window not large enough for current row\n", + startPos + numRows); + if(!failed && (startPos + numRows) < requiredPos) { + LOG_WINDOW("Reseting window, previously unable to map required row %d into window\n", + requiredPos); + i = 0; + window->clear(); + window->setNumColumns(numColumns); + field_slot_t * fieldDir = window->allocRow(); + if(!fieldDir) { + LOG_WINDOW("Failed to allocate row in reset, bailing\n"); + jniThrowException(env, "net/sqlcipher/RowAllocationException", + "Failed to allocate row in reset within native_fill_window"); + } else { + LOG_WINDOW("Allocated row in reset set\n"); + } + } else { + LOG_WINDOW("Bailing from reset, requested row %d already mapped in cursor window\n", + startPos + numRows); + return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + } + failed = true; + reset = false; + } + int type = sqlite3_column_type(statement, i); if (type == SQLITE_TEXT) { // TEXT data @@ -197,7 +234,8 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, window->freeLastRow(); LOGE("Failed allocating %u bytes for text/blob at %d,%d", size, startPos + numRows, i); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + reset = true; + continue; } window->copyIn(offset, text, size); @@ -216,7 +254,8 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, if (!window->putLong(numRows, i, value)) { window->freeLastRow(); LOGE("Failed allocating space for a long in column %d", i); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + reset = true; + continue; } LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + numRows, i, value); } else if (type == SQLITE_FLOAT) { @@ -225,7 +264,8 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, if (!window->putDouble(numRows, i, value)) { window->freeLastRow(); LOGE("Failed allocating space for a double in column %d", i); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + reset = true; + continue; } LOG_WINDOW("%d,%d is FLOAT %lf", startPos + numRows, i, value); } else if (type == SQLITE_BLOB) { @@ -237,11 +277,10 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, window->freeLastRow(); LOGE("Failed allocating %u bytes for blob at %d,%d", size, startPos + numRows, i); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + reset = true; + continue; } - window->copyIn(offset, blob, size); - // This must be updated after the call to alloc(), since that // may move the field around in the window field_slot_t * fieldSlot = window->getFieldSlot(numRows, i); @@ -264,9 +303,9 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, } if (i < numColumns) { - // Not all the fields fit in the window - // Unknown data error happened - break; + // Not all the fields fit in the window + // Unknown data error happened + break; } // Mark the row as complete in the window @@ -326,7 +365,7 @@ static jstring native_column_name(JNIEnv* env, jobject object, jint columnIndex) static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ - {"native_fill_window", "(Lnet/sqlcipher/CursorWindow;IIII)I", (void *)native_fill_window}, + {"native_fill_window", "(Lnet/sqlcipher/CursorWindow;IIIII)I", (void *)native_fill_window}, {"native_column_count", "()I", (void*)native_column_count}, {"native_column_name", "(I)Ljava/lang/String;", (void *)native_column_name}, }; diff --git a/jni/net_sqlcipher_database_SQLiteStatement.cpp b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteStatement.cpp similarity index 95% rename from jni/net_sqlcipher_database_SQLiteStatement.cpp rename to android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteStatement.cpp index c4e757e6..6f6ca32c 100644 --- a/jni/net_sqlcipher_database_SQLiteStatement.cpp +++ b/android-database-sqlcipher/src/main/cpp/net_sqlcipher_database_SQLiteStatement.cpp @@ -55,7 +55,9 @@ static void native_execute(JNIEnv* env, jobject object) // Throw an exception if an error occured if (err != SQLITE_DONE) { - throw_sqlite3_exception_errcode(env, err, sqlite3_errmsg(handle)); + throw_sqlite3_exception_errcode(env, err, + sqlite3_extended_errcode(handle), + sqlite3_errmsg(handle)); } // Reset the statment so it's ready to use again diff --git a/jni/sqlcipher_loading.h b/android-database-sqlcipher/src/main/cpp/sqlcipher_loading.h similarity index 100% rename from jni/sqlcipher_loading.h rename to android-database-sqlcipher/src/main/cpp/sqlcipher_loading.h diff --git a/jni/sqlite3_exception.h b/android-database-sqlcipher/src/main/cpp/sqlite3_exception.h similarity index 88% rename from jni/sqlite3_exception.h rename to android-database-sqlcipher/src/main/cpp/sqlite3_exception.h index 866d9824..db31e571 100644 --- a/jni/sqlite3_exception.h +++ b/android-database-sqlcipher/src/main/cpp/sqlite3_exception.h @@ -42,6 +42,10 @@ void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* messa void throw_sqlite3_exception(JNIEnv* env, int errcode, const char* sqlite3Message, const char* message); -} +void throw_sqlite3_exception_errcode(JNIEnv* env, + int errcode, + int extended_err_code, + const char* message); +} #endif // _SQLITE3_EXCEPTION_H diff --git a/src/net/sqlcipher/AbstractCursor.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/AbstractCursor.java similarity index 99% rename from src/net/sqlcipher/AbstractCursor.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/AbstractCursor.java index d157edf9..f3eaf8aa 100644 --- a/src/net/sqlcipher/AbstractCursor.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/AbstractCursor.java @@ -77,11 +77,11 @@ public CursorWindow getWindow() { public int getColumnCount() { return getColumnNames().length; } - + public void deactivate() { deactivateInternal(); } - + /** * @hide */ @@ -92,10 +92,10 @@ public void deactivateInternal() { } mDataSetObservable.notifyInvalidated(); } - + public boolean requery() { if (mSelfObserver != null && mSelfObserverRegistered == false) { - + mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); mSelfObserverRegistered = true; } @@ -106,7 +106,7 @@ public boolean requery() { public boolean isClosed() { return mClosed; } - + public void close() { mClosed = true; mContentObservable.unregisterAll(); @@ -143,7 +143,7 @@ public boolean onMove(int oldPosition, int newPosition) { return true; } - + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { // Default implementation, uses getString String result = getString(columnIndex); @@ -159,7 +159,7 @@ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { buffer.sizeCopied = 0; } } - + /* -------------------------------------------------------- */ /* Implementation */ public AbstractCursor() { @@ -204,7 +204,7 @@ public final boolean moveToPosition(int position) { return result; } - + /** * Copy data from cursor to CursorWindow * @param position start position of data @@ -388,7 +388,7 @@ public boolean update(int columnIndex, Object obj) { /** * Returns true if there are pending updates that have not yet been committed. - * + * * @return true if there are pending updates that have not yet been committed. * @hide * @deprecated @@ -435,7 +435,7 @@ public void unregisterContentObserver(ContentObserver observer) { mContentObservable.unregisterObserver(observer); } } - + /** * This is hidden until the data set change model has been re-evaluated. * @hide @@ -443,18 +443,18 @@ public void unregisterContentObserver(ContentObserver observer) { protected void notifyDataSetChange() { mDataSetObservable.notifyChanged(); } - + /** * This is hidden until the data set change model has been re-evaluated. * @hide */ protected DataSetObservable getDataSetObservable() { return mDataSetObservable; - + } public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); - + } public void unregisterDataSetObserver(DataSetObserver observer) { diff --git a/src/net/sqlcipher/AbstractWindowedCursor.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/AbstractWindowedCursor.java similarity index 99% rename from src/net/sqlcipher/AbstractWindowedCursor.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/AbstractWindowedCursor.java index ec48e709..deb25f33 100644 --- a/src/net/sqlcipher/AbstractWindowedCursor.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/AbstractWindowedCursor.java @@ -50,18 +50,18 @@ public String getString(int columnIndex) return mWindow.getString(mPos, columnIndex); } - + @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { checkPosition(); - + synchronized(mUpdatedRows) { if (isFieldUpdated(columnIndex)) { super.copyStringToBuffer(columnIndex, buffer); } } - + mWindow.copyStringToBuffer(mPos, columnIndex, buffer); } @@ -220,7 +220,7 @@ public int getType(int columnIndex) { protected void checkPosition() { super.checkPosition(); - + if (mWindow == null) { throw new StaleDataException("Access closed cursor"); } @@ -230,7 +230,7 @@ protected void checkPosition() public CursorWindow getWindow() { return mWindow; } - + /** * Set a new cursor window to cursor, usually set a remote cursor window * @param window cursor window @@ -241,7 +241,7 @@ public void setWindow(CursorWindow window) { } mWindow = window; } - + public boolean hasWindow() { return mWindow != null; } diff --git a/src/net/sqlcipher/BulkCursorNative.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/BulkCursorNative.java similarity index 99% rename from src/net/sqlcipher/BulkCursorNative.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/BulkCursorNative.java index 9c8e2f1a..868dde6a 100644 --- a/src/net/sqlcipher/BulkCursorNative.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/BulkCursorNative.java @@ -28,7 +28,7 @@ /** * Native implementation of the bulk cursor. This is only for use in implementing * IPC, application code should use the Cursor interface. - * + * * {@hide} */ public abstract class BulkCursorNative extends Binder implements IBulkCursor @@ -54,7 +54,7 @@ static public IBulkCursor asInterface(IBinder obj) return new BulkCursorProxy(obj); } - + @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { @@ -100,7 +100,7 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) reply.writeNoException(); return true; } - + case CLOSE_TRANSACTION: { data.enforceInterface(IBulkCursor.descriptor); close(); @@ -215,7 +215,7 @@ public CursorWindow getWindow(int startPos) throws RemoteException mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); - + CursorWindow window = null; if (reply.readInt() == 1) { window = CursorWindow.newFromParcel(reply); @@ -253,7 +253,7 @@ public int count() throws RemoteException boolean result = mRemote.transact(COUNT_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); - + int count; if (result == false) { count = -1; @@ -275,14 +275,14 @@ public String[] getColumnNames() throws RemoteException mRemote.transact(GET_COLUMN_NAMES_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); - + String[] columnNames = null; int numColumns = reply.readInt(); columnNames = new String[numColumns]; for (int i = 0; i < numColumns; i++) { columnNames[i] = reply.readString(); } - + data.recycle(); reply.recycle(); return columnNames; @@ -315,7 +315,7 @@ public void close() throws RemoteException data.recycle(); reply.recycle(); } - + public int requery(IContentObserver observer, CursorWindow window) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -326,7 +326,7 @@ public int requery(IContentObserver observer, CursorWindow window) throws Remote window.writeToParcel(data, 0); boolean result = mRemote.transact(REQUERY_TRANSACTION, data, reply, 0); - + DatabaseUtils.readExceptionFromParcel(reply); int count; @@ -355,7 +355,7 @@ public boolean updateRows(Map values) throws RemoteException mRemote.transact(UPDATE_ROWS_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); - + boolean result = (reply.readInt() == 1 ? true : false); data.recycle(); @@ -376,7 +376,7 @@ public boolean deleteRow(int position) throws RemoteException mRemote.transact(DELETE_ROW_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); - + boolean result = (reply.readInt() == 1 ? true : false); data.recycle(); diff --git a/src/net/sqlcipher/BulkCursorToCursorAdaptor.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/BulkCursorToCursorAdaptor.java similarity index 99% rename from src/net/sqlcipher/BulkCursorToCursorAdaptor.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/BulkCursorToCursorAdaptor.java index c5753daf..932507e3 100644 --- a/src/net/sqlcipher/BulkCursorToCursorAdaptor.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/BulkCursorToCursorAdaptor.java @@ -139,7 +139,7 @@ public void deactivate() { } mWindow = null; } - + @Override public void close() { super.close(); @@ -148,7 +148,7 @@ public void close() { } catch (RemoteException ex) { Log.w(TAG, "Remote process exception when closing"); } - mWindow = null; + mWindow = null; } @Override @@ -189,7 +189,7 @@ public boolean deleteRow() { if (result != false) { // The window contains the old value, discard it mWindow = null; - + // Fix up the position mCount = mBulkCursor.count(); if (mPos < mCount) { @@ -246,7 +246,7 @@ public boolean commitUpdates(Maprow, col) as a String. * @@ -406,11 +433,11 @@ public String getString(int row, int col) { /** * copy the text for the given field in the provided char array. - * - * @param row the row to read from, row - getStartPosition() being the actual row in the window + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from - * @param buffer the CharArrayBuffer to copy the text into, - * If the requested string is larger than the buffer + * @param buffer the CharArrayBuffer to copy the text into, + * If the requested string is larger than the buffer * a new char buffer will be created to hold the string. and assigne to * CharArrayBuffer.data */ @@ -432,15 +459,15 @@ public void copyStringToBuffer(int row, int col, CharArrayBuffer buffer) { releaseReference(); } } - + private native char[] copyStringToBuffer_native( int row, int col, int bufferSize, CharArrayBuffer buffer); - + /** * Returns a long for the given field. * row is 0 based - * - * @param row the row to read from, row - getStartPosition() being the actual row in the window + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return a long value for the given field */ @@ -452,7 +479,7 @@ public long getLong(int row, int col) { releaseReference(); } } - + /** * Returns the value at (row, col) as a long. * @@ -469,8 +496,8 @@ public long getLong(int row, int col) { /** * Returns a double for the given field. * row is 0 based - * - * @param row the row to read from, row - getStartPosition() being the actual row in the window + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return a double value for the given field */ @@ -482,7 +509,7 @@ public double getDouble(int row, int col) { releaseReference(); } } - + /** * Returns the value at (row, col) as a double. * @@ -499,8 +526,8 @@ public double getDouble(int row, int col) { /** * Returns a short for the given field. * row is 0 based - * - * @param row the row to read from, row - getStartPosition() being the actual row in the window + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return a short value for the given field */ @@ -515,8 +542,8 @@ public short getShort(int row, int col) { /** * Returns an int for the given field. - * - * @param row the row to read from, row - getStartPosition() being the actual row in the window + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return an int value for the given field */ @@ -528,12 +555,12 @@ public int getInt(int row, int col) { releaseReference(); } } - + /** * Returns a float for the given field. * row is 0 based - * - * @param row the row to read from, row - getStartPosition() being the actual row in the window + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window * @param col the column to read from * @return a float value for the given field */ @@ -544,8 +571,8 @@ public float getFloat(int row, int col) { } finally { releaseReference(); } - } - + } + /** * Clears out the existing contents of the window, making it safe to reuse * for new data. Note that the number of columns in the window may NOT @@ -554,7 +581,7 @@ public float getFloat(int row, int col) { public void clear() { acquireReference(); try { - mStartPos = 0; + mStartPos = 0; native_clear(); } finally { releaseReference(); @@ -570,7 +597,7 @@ public void clear() { public void close() { releaseReference(); } - + private native void close_native(); @Override @@ -581,7 +608,7 @@ protected void finalize() { } close_native(); } - + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public CursorWindow createFromParcel(Parcel source) { @@ -607,9 +634,9 @@ public void writeToParcel(Parcel dest, int flags) { } public CursorWindow(Parcel source,int foo) { - + super(true); - + IBinder nativeBinder = source.readStrongBinder(); mStartPos = source.readInt(); @@ -620,7 +647,8 @@ public CursorWindow(Parcel source,int foo) { private native IBinder native_getBinder(); /** Does the native side initialization for an empty window */ - private native void native_init(boolean localOnly); + private native void native_init(boolean localOnly, long initialSize, + long growthPaddingSize, long maxSize); /** Does the native side initialization with an existing binder from another process */ private native void native_init(IBinder nativeBinder); diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/CursorWindowAllocation.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/CursorWindowAllocation.java new file mode 100644 index 00000000..6b4c47f2 --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/CursorWindowAllocation.java @@ -0,0 +1,7 @@ +package net.sqlcipher; + +public interface CursorWindowAllocation { + long getInitialAllocationSize(); + long getGrowthPaddingSize(); + long getMaxAllocationSize(); +} diff --git a/src/net/sqlcipher/CursorWrapper.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/CursorWrapper.java similarity index 100% rename from src/net/sqlcipher/CursorWrapper.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/CursorWrapper.java diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/CustomCursorWindowAllocation.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/CustomCursorWindowAllocation.java new file mode 100644 index 00000000..9575dafe --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/CustomCursorWindowAllocation.java @@ -0,0 +1,30 @@ +package net.sqlcipher; + +import net.sqlcipher.CursorWindowAllocation; + +public class CustomCursorWindowAllocation implements CursorWindowAllocation { + + private long initialAllocationSize = 0L; + private long growthPaddingSize = 0L; + private long maxAllocationSize = 0L; + + public CustomCursorWindowAllocation(long initialSize, + long growthPaddingSize, + long maxAllocationSize){ + this.initialAllocationSize = initialSize; + this.growthPaddingSize = growthPaddingSize; + this.maxAllocationSize = maxAllocationSize; + } + + public long getInitialAllocationSize() { + return initialAllocationSize; + } + + public long getGrowthPaddingSize() { + return growthPaddingSize; + } + + public long getMaxAllocationSize() { + return maxAllocationSize; + } +} diff --git a/src/net/sqlcipher/DatabaseErrorHandler.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/DatabaseErrorHandler.java similarity index 100% rename from src/net/sqlcipher/DatabaseErrorHandler.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/DatabaseErrorHandler.java diff --git a/src/net/sqlcipher/DatabaseUtils.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/DatabaseUtils.java similarity index 98% rename from src/net/sqlcipher/DatabaseUtils.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/DatabaseUtils.java index 401e8bea..a89bebab 100644 --- a/src/net/sqlcipher/DatabaseUtils.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/DatabaseUtils.java @@ -16,13 +16,7 @@ package net.sqlcipher; -import net.sqlcipher.database.SQLiteAbortException; -import net.sqlcipher.database.SQLiteConstraintException; import net.sqlcipher.database.SQLiteDatabase; -import net.sqlcipher.database.SQLiteDatabaseCorruptException; -import net.sqlcipher.database.SQLiteDiskIOException; -import net.sqlcipher.database.SQLiteException; -import net.sqlcipher.database.SQLiteFullException; import net.sqlcipher.database.SQLiteProgram; import net.sqlcipher.database.SQLiteStatement; @@ -34,6 +28,14 @@ import android.content.ContentValues; import android.content.OperationApplicationException; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteAbortException; +import android.database.sqlite.SQLiteConstraintException; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.database.sqlite.SQLiteDiskIOException; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteFullException; import android.os.Parcel; import android.text.TextUtils; import android.util.Config; @@ -71,7 +73,7 @@ public static final void writeExceptionToParcel(Parcel reply, Exception e) { code = 3; } else if (e instanceof SQLiteAbortException) { code = 4; - } else if (e instanceof SQLiteConstraintException) { + } else if (e instanceof android.database.sqlite.SQLiteConstraintException) { code = 5; } else if (e instanceof SQLiteDatabaseCorruptException) { code = 6; @@ -1217,10 +1219,10 @@ public static void cursorFillWindow(final Cursor cursor, /* static public void createDbFromSqlStatements( Context context, String dbName, int dbVersion, String sqlStatements) { - + //TODO TODO TODO what needs ot happen here SQLiteDatabase db = context.openOrCreateDatabase(dbName, 0, null); - + // TODO: this is not quite safe since it assumes that all semicolons at the end of a line // terminate statements. It is possible that a text field contains ;\n. We will have to fix // this if that turns out to be a problem. diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/DefaultCursorWindowAllocation.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/DefaultCursorWindowAllocation.java new file mode 100644 index 00000000..47b5f548 --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/DefaultCursorWindowAllocation.java @@ -0,0 +1,21 @@ +package net.sqlcipher; + +import net.sqlcipher.CursorWindowAllocation; + +public class DefaultCursorWindowAllocation implements CursorWindowAllocation { + + private long initialAllocationSize = 1024 * 1024; + private long WindowAllocationUnbounded = 0; + + public long getInitialAllocationSize() { + return initialAllocationSize; + } + + public long getGrowthPaddingSize() { + return initialAllocationSize; + } + + public long getMaxAllocationSize() { + return WindowAllocationUnbounded; + } +} diff --git a/src/net/sqlcipher/DefaultDatabaseErrorHandler.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/DefaultDatabaseErrorHandler.java similarity index 98% rename from src/net/sqlcipher/DefaultDatabaseErrorHandler.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/DefaultDatabaseErrorHandler.java index d9f4e0d7..f9de1b52 100644 --- a/src/net/sqlcipher/DefaultDatabaseErrorHandler.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/DefaultDatabaseErrorHandler.java @@ -20,8 +20,8 @@ import java.util.List; import net.sqlcipher.database.SQLiteDatabase; -import net.sqlcipher.database.SQLiteException; +import android.database.sqlite.SQLiteException; import android.util.Log; import android.util.Pair; diff --git a/src/net/sqlcipher/IBulkCursor.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/IBulkCursor.java similarity index 100% rename from src/net/sqlcipher/IBulkCursor.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/IBulkCursor.java diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/InvalidRowColumnException.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/InvalidRowColumnException.java new file mode 100644 index 00000000..275b28d9 --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/InvalidRowColumnException.java @@ -0,0 +1,14 @@ +package net.sqlcipher; + +/** + * An exception that indicates there was an error accessing a specific row/column. + */ +public class InvalidRowColumnException extends RuntimeException +{ + public InvalidRowColumnException() {} + + public InvalidRowColumnException(String error) + { + super(error); + } +} diff --git a/src/net/sqlcipher/MatrixCursor.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/MatrixCursor.java similarity index 100% rename from src/net/sqlcipher/MatrixCursor.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/MatrixCursor.java diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/RowAllocationException.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/RowAllocationException.java new file mode 100644 index 00000000..a4680568 --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/RowAllocationException.java @@ -0,0 +1,15 @@ +package net.sqlcipher; + +/** + * An exception that indicates there was an error attempting to allocate a row + * for the CursorWindow. + */ +public class RowAllocationException extends RuntimeException +{ + public RowAllocationException() {} + + public RowAllocationException(String error) + { + super(error); + } +} diff --git a/src/net/sqlcipher/StaleDataException.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/StaleDataException.java similarity index 100% rename from src/net/sqlcipher/StaleDataException.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/StaleDataException.java diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/UnknownTypeException.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/UnknownTypeException.java new file mode 100644 index 00000000..4da359ff --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/UnknownTypeException.java @@ -0,0 +1,14 @@ +package net.sqlcipher; + +/** + * An exception that indicates an unknown type was returned. + */ +public class UnknownTypeException extends RuntimeException +{ + public UnknownTypeException() {} + + public UnknownTypeException(String error) + { + super(error); + } +} diff --git a/src/net/sqlcipher/database/DatabaseObjectNotClosedException.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/DatabaseObjectNotClosedException.java similarity index 100% rename from src/net/sqlcipher/database/DatabaseObjectNotClosedException.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/DatabaseObjectNotClosedException.java diff --git a/src/net/sqlcipher/database/SQLiteClosable.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteClosable.java similarity index 100% rename from src/net/sqlcipher/database/SQLiteClosable.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteClosable.java diff --git a/src/net/sqlcipher/database/SQLiteCompiledSql.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCompiledSql.java similarity index 89% rename from src/net/sqlcipher/database/SQLiteCompiledSql.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCompiledSql.java index 1eed5b4b..52787c14 100644 --- a/src/net/sqlcipher/database/SQLiteCompiledSql.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCompiledSql.java @@ -48,7 +48,6 @@ /** the following are for debugging purposes */ private String mSqlStmt = null; - private Throwable mStackTrace = null; /** when in cache and is in use, this member is set */ private boolean mInUse = false; @@ -59,7 +58,6 @@ } mDatabase = db; mSqlStmt = sql; - mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); this.nHandle = db.mNativeHandle; compile(sql, true); } @@ -95,20 +93,15 @@ private void compile(String sql, boolean forceCompilation) { } } - /* package */ void releaseSqlStatement() { + /* package */ synchronized void releaseSqlStatement() { // Note that native_finalize() checks to make sure that nStatement is // non-null before destroying it. if (nStatement != 0) { if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { Log.v(TAG, "closed and deallocated DbObj (id#" + nStatement +")"); } - try { - mDatabase.lock(); - native_finalize(); - nStatement = 0; - } finally { - mDatabase.unlock(); - } + native_finalize(); + nStatement = 0; } } @@ -145,10 +138,6 @@ protected void finalize() throws Throwable { if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { Log.v(TAG, "** warning ** Finalized DbObj (id#" + nStatement + ")"); } - int len = mSqlStmt.length(); - Log.w(TAG, "Releasing statement in a finalizer. Please ensure " + - "that you explicitly call close() on your cursor: " + - mSqlStmt.substring(0, (len > 100) ? 100 : len), mStackTrace); releaseSqlStatement(); } finally { super.finalize(); diff --git a/src/net/sqlcipher/database/SQLiteContentHelper.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteContentHelper.java similarity index 99% rename from src/net/sqlcipher/database/SQLiteContentHelper.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteContentHelper.java index 09c9114e..3300b610 100644 --- a/src/net/sqlcipher/database/SQLiteContentHelper.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteContentHelper.java @@ -61,7 +61,7 @@ public static AssetFileDescriptor getBlobColumnAsAssetFile(SQLiteDatabase db, St fd = (android.os.ParcelFileDescriptor)m.invoke(file); } catch (Exception e) { android.util.Log.i("SQLiteContentHelper", "SQLiteCursor.java: " + e); - } + } AssetFileDescriptor afd = new AssetFileDescriptor(fd, 0, file.length()); return afd; } catch (IOException ex) { @@ -95,7 +95,7 @@ private static MemoryFile simpleQueryForBlobMemoryFile(SQLiteDatabase db, String } MemoryFile file = new MemoryFile(null, bytes.length); file.writeBytes(bytes, 0, 0, bytes.length); - + // file.deactivate(); return file; } finally { diff --git a/src/net/sqlcipher/database/SQLiteCursor.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCursor.java similarity index 80% rename from src/net/sqlcipher/database/SQLiteCursor.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCursor.java index fe318e03..a216d986 100644 --- a/src/net/sqlcipher/database/SQLiteCursor.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCursor.java @@ -17,8 +17,8 @@ package net.sqlcipher.database; import net.sqlcipher.AbstractWindowedCursor; +import net.sqlcipher.BuildConfig; import net.sqlcipher.CursorWindow; -import net.sqlcipher.SQLException; import java.lang.ref.WeakReference; import java.util.HashMap; @@ -28,6 +28,7 @@ import android.database.CharArrayBuffer; import android.database.DataSetObserver; +import android.database.SQLException; import android.os.Handler; import android.os.Message; import android.os.Process; @@ -64,14 +65,18 @@ public class SQLiteCursor extends AbstractWindowedCursor { /** The number of rows in the cursor */ private int mCount = NO_COUNT; + private int mCursorWindowCapacity = 0; + + private boolean fillWindowForwardOnly = false; + /** A mapping of column names to column indices, to speed up lookups */ private Map mColumnNameMap; /** Used to find out where a cursor was allocated in case it never got released. */ private Throwable mStackTrace; - - /** - * mMaxRead is the max items that each cursor window reads + + /** + * mMaxRead is the max items that each cursor window reads * default to a very high value */ private int mMaxRead = Integer.MAX_VALUE; @@ -79,13 +84,17 @@ public class SQLiteCursor extends AbstractWindowedCursor { private int mCursorState = 0; private ReentrantLock mLock = null; private boolean mPendingData = false; - + + public void setFillWindowForwardOnly(boolean value) { + fillWindowForwardOnly = value; + } + /** * support for a cursor variant that doesn't always read all results - * initialRead is the initial number of items that cursor window reads + * initialRead is the initial number of items that cursor window reads * if query contains more than this number of items, a thread will be - * created and handle the left over items so that caller can show - * results as soon as possible + * created and handle the left over items so that caller can show + * results as soon as possible * @param initialRead initial number of items that cursor read * @param maxRead leftover items read at maxRead items per time * @hide @@ -95,20 +104,20 @@ public void setLoadStyle(int initialRead, int maxRead) { mInitialRead = initialRead; mLock = new ReentrantLock(true); } - + private void queryThreadLock() { if (mLock != null) { - mLock.lock(); + mLock.lock(); } } - + private void queryThreadUnlock() { if (mLock != null) { - mLock.unlock(); + mLock.unlock(); } } - - + + /** * @hide */ @@ -124,7 +133,7 @@ private void sendMessage() { } else { mPendingData = true; } - + } public void run() { // use cached mWindow, to avoid get null mWindow @@ -132,6 +141,9 @@ public void run() { Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND); // the cursor's state doesn't change while (true) { + if(mLock == null){ + mLock = new ReentrantLock(true); + } mLock.lock(); if (mCursorState != mThreadState) { mLock.unlock(); @@ -144,7 +156,7 @@ public void run() { if (count == NO_COUNT){ mCount += mMaxRead; sendMessage(); - } else { + } else { mCount = count; sendMessage(); break; @@ -159,13 +171,13 @@ public void run() { mLock.unlock(); } } - } + } } - - + + /** * @hide - */ + */ protected static class MainThreadNotificationHandler extends Handler { private final WeakReference wrappedCursor; @@ -181,15 +193,15 @@ public void handleMessage(Message msg) { } } } - + /** * @hide */ - protected MainThreadNotificationHandler mNotificationHandler; - + protected MainThreadNotificationHandler mNotificationHandler; + public void registerDataSetObserver(DataSetObserver observer) { super.registerDataSetObserver(observer); - if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) && + if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) && mNotificationHandler == null) { queryThreadLock(); try { @@ -202,9 +214,9 @@ public void registerDataSetObserver(DataSetObserver observer) { queryThreadUnlock(); } } - + } - + /** * Execute a query and provide access to its result set through a Cursor * interface. For a query such as: {@code SELECT name, birth, phone FROM @@ -241,11 +253,11 @@ public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, for (int i = 0; i < columnCount; i++) { String columnName = mQuery.columnNameLocked(i); mColumns[i] = columnName; - if (Config.LOGV) { + if(BuildConfig.DEBUG){ Log.v("DatabaseWindow", "mColumns[" + i + "] is " + mColumns[i]); } - + // Make note of the row ID column index for quick access to it if ("_id".equals(columnName)) { mRowIdColumnIndex = i; @@ -282,7 +294,8 @@ public int getCount() { return mCount; } - private void fillWindow (int startPos) { + private void fillWindow (int requiredPos) { + int startPos = 0; if (mWindow == null) { // If there isn't a window set already it will only be accessed locally mWindow = new CursorWindow(true /* the window is local only */); @@ -295,14 +308,29 @@ private void fillWindow (int startPos) { queryThreadUnlock(); } } + if(fillWindowForwardOnly) { + startPos = requiredPos; + } else { + startPos = mCount == NO_COUNT + ? cursorPickFillWindowStartPosition(requiredPos, 0) + : cursorPickFillWindowStartPosition(requiredPos, mCursorWindowCapacity); + } mWindow.setStartPosition(startPos); + mWindow.setRequiredPosition(requiredPos); + if(BuildConfig.DEBUG){ + Log.v(TAG, String.format("Filling cursor window with start position:%d required position:%d", + startPos, requiredPos)); + } mCount = mQuery.fillWindow(mWindow, mInitialRead, 0); + if(mCursorWindowCapacity == 0) { + mCursorWindowCapacity = mWindow.getNumRows(); + } // return -1 means not finished if (mCount == NO_COUNT){ mCount = startPos + mInitialRead; Thread t = new Thread(new QueryThread(mCursorState), "query thread"); t.start(); - } + } } @Override @@ -322,8 +350,10 @@ 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); - columnName = columnName.substring(periodIndex + 1); + if(BuildConfig.DEBUG){ + Log.e(TAG, "requesting column name with table name -- " + columnName, e); + columnName = columnName.substring(periodIndex + 1); + } } Integer i = mColumnNameMap.get(columnName); @@ -344,10 +374,12 @@ public boolean deleteRow() { // Only allow deletes if there is an ID column, and the ID has been read from it if (mRowIdColumnIndex == -1 || mCurrentRowID == null) { + if(BuildConfig.DEBUG){ Log.e(TAG, - "Could not delete row because either the row ID column is not available or it" + - "has not been read."); - return false; + "Could not delete row because either the row ID column is not available or it" + + "has not been read."); + } + return false; } boolean success; @@ -411,9 +443,11 @@ public boolean supportsUpdates() { public boolean commitUpdates(Map> additionalValues) { if (!supportsUpdates()) { + if(BuildConfig.DEBUG){ Log.e(TAG, "commitUpdates not supported on this cursor, did you " - + "include the _id column?"); - return false; + + "include the _id column?"); + } + return false; } /* @@ -496,13 +530,13 @@ public boolean commitUpdates(Map 100) ? 100 : len), mStackTrace); + } close(); SQLiteDebug.notifyActiveCursorFinalized(); } else { - if (Config.LOGV) { - Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() + - ", table = " + mEditTable + ", query = " + mQuery.mSql); + if(BuildConfig.DEBUG) { + Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() + + ", table = " + mEditTable + ", query = " + mQuery.mSql); } } } finally { @@ -615,42 +651,50 @@ protected void finalize() { } - + @Override - public void fillWindow(int startPos, android.database.CursorWindow window) { - - /* - window.setStartPosition(startPos); - mCount = mQuery.fillWindow((net.sqlcipher.database.CursorWindow)window, mInitialRead, 0); - // return -1 means not finished - if (mCount == NO_COUNT){ - mCount = startPos + mInitialRead; - Thread t = new Thread(new QueryThread(mCursorState), "query thread"); - t.start(); - } */ - - if (mWindow == null) { - // If there isn't a window set already it will only be accessed locally - mWindow = new CursorWindow(true /* the window is local only */); - } else { - mCursorState++; - queryThreadLock(); - try { - mWindow.clear(); - } finally { - queryThreadUnlock(); - } - } - mWindow.setStartPosition(startPos); - mCount = mQuery.fillWindow(mWindow, mInitialRead, 0); - // return -1 means not finished - if (mCount == NO_COUNT){ - mCount = startPos + mInitialRead; - Thread t = new Thread(new QueryThread(mCursorState), "query thread"); - t.start(); + public void fillWindow(int requiredPos, android.database.CursorWindow window) { + int startPos = 0; + if (mWindow == null) { + // If there isn't a window set already it will only be accessed locally + mWindow = new CursorWindow(true /* the window is local only */); + } else { + mCursorState++; + queryThreadLock(); + try { + mWindow.clear(); + } finally { + queryThreadUnlock(); } - - + } + if(fillWindowForwardOnly) { + startPos = requiredPos; + } else { + startPos = mCount == NO_COUNT + ? cursorPickFillWindowStartPosition(requiredPos, 0) + : cursorPickFillWindowStartPosition(requiredPos, mCursorWindowCapacity); + } + mWindow.setStartPosition(startPos); + mWindow.setRequiredPosition(requiredPos); + if(BuildConfig.DEBUG) { + Log.v(TAG, String.format("Filling cursor window with start position:%d required position:%d", + startPos, requiredPos)); + } + mCount = mQuery.fillWindow(mWindow, mInitialRead, 0); + if(mCursorWindowCapacity == 0) { + mCursorWindowCapacity = mWindow.getNumRows(); + } + // return -1 means not finished + if (mCount == NO_COUNT){ + mCount = startPos + mInitialRead; + Thread t = new Thread(new QueryThread(mCursorState), "query thread"); + t.start(); + } } + public int cursorPickFillWindowStartPosition( + int cursorPosition, int cursorWindowCapacity) { + return Math.max(cursorPosition - cursorWindowCapacity / 3, 0); + } + } diff --git a/src/net/sqlcipher/database/SQLiteCursorDriver.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCursorDriver.java similarity index 99% rename from src/net/sqlcipher/database/SQLiteCursorDriver.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCursorDriver.java index 93ad1503..1ea66aba 100644 --- a/src/net/sqlcipher/database/SQLiteCursorDriver.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteCursorDriver.java @@ -26,7 +26,7 @@ public interface SQLiteCursorDriver { /** * Executes the query returning a Cursor over the result set. - * + * * @param factory The CursorFactory to use when creating the Cursors, or * null if standard SQLiteCursors should be returned. * @return a Cursor over the result set @@ -40,7 +40,7 @@ public interface SQLiteCursorDriver { /** * Called by a SQLiteCursor when it is requeryed. - * + * * @return The new count value. */ void cursorRequeried(android.database.Cursor cursor); diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDatabase.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDatabase.java new file mode 100644 index 00000000..a4419653 --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDatabase.java @@ -0,0 +1,3272 @@ +/* + * 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. + */ + +package net.sqlcipher.database; + +import net.sqlcipher.BuildConfig; +import net.sqlcipher.Cursor; +import net.sqlcipher.CrossProcessCursorWrapper; +import net.sqlcipher.DatabaseUtils; +import net.sqlcipher.DatabaseErrorHandler; +import net.sqlcipher.DefaultDatabaseErrorHandler; +import net.sqlcipher.database.SQLiteStatement; +import net.sqlcipher.database.SQLiteDebug.DbStats; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.sqlcipher.database.SQLiteQueryStats; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Pattern; +import java.util.zip.ZipInputStream; + +import android.content.ContentValues; + +import android.content.Context; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.database.sqlite.SQLiteException; + +import android.os.CancellationSignal; +import android.os.Debug; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; +import android.util.Pair; + +import java.io.UnsupportedEncodingException; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteQuery; + +/** + * Exposes methods to manage a SQLCipher database. + *

SQLiteDatabase has methods to create, delete, execute SQL commands, and + * perform other common database management tasks. + *

A call to loadLibs(…) should occur before attempting to + * create or open a database connection. + *

Database names must be unique within an application, not across all + * applications. + * + */ +public class SQLiteDatabase extends SQLiteClosable implements + SupportSQLiteDatabase { + private static final String TAG = "Database"; + private static final int EVENT_DB_OPERATION = 52000; + private static final int EVENT_DB_CORRUPT = 75004; + private static final String KEY_ENCODING = "UTF-8"; + + private enum SQLiteDatabaseTransactionType { + Deferred, + Immediate, + Exclusive, + } + + /** + * The version number of the SQLCipher for Android Java client library. + */ + public static final String SQLCIPHER_ANDROID_VERSION = BuildConfig.VERSION_NAME; + + // Stores reference to all databases opened in the current process. + // (The referent Object is not used at this time.) + // INVARIANT: Guarded by sActiveDatabases. + private static WeakHashMap sActiveDatabases = + new WeakHashMap(); + + public int status(int operation, boolean reset){ + return native_status(operation, reset); + } + + /** + * Change the password of the open database using sqlite3_rekey(). + * + * @param password new database password + * + * @throws SQLiteException if there is an issue changing the password internally + * OR if the database is not open + * + * FUTURE @todo throw IllegalStateException if the database is not open and + * update the test suite + */ + public void changePassword(String password) throws SQLiteException { + /* safeguard: */ + if (!isOpen()) { + throw new SQLiteException("database not open"); + } + if (password != null) { + byte[] keyMaterial = getBytes(password.toCharArray()); + rekey(keyMaterial); + Arrays.fill(keyMaterial, (byte) 0); + } + } + + /** + * Change the password of the open database using sqlite3_rekey(). + * + * @param password new database password (char array) + * + * @throws SQLiteException if there is an issue changing the password internally + * OR if the database is not open + * + * FUTURE @todo throw IllegalStateException if the database is not open and + * update the test suite + */ + public void changePassword(char[] password) throws SQLiteException { + /* safeguard: */ + if (!isOpen()) { + throw new SQLiteException("database not open"); + } + if (password != null) { + byte[] keyMaterial = getBytes(password); + rekey(keyMaterial); + Arrays.fill(keyMaterial, (byte) 0); + } + } + + private static void loadICUData(Context context, File workingDir) { + OutputStream out = null; + ZipInputStream in = null; + File icuDir = new File(workingDir, "icu"); + File icuDataFile = new File(icuDir, "icudt46l.dat"); + try { + if(!icuDir.exists()) icuDir.mkdirs(); + if(!icuDataFile.exists()) { + in = new ZipInputStream(context.getAssets().open("icudt46l.zip")); + in.getNextEntry(); + out = new FileOutputStream(icuDataFile); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } + } + catch (Exception ex) { + if(BuildConfig.DEBUG){ + Log.e(TAG, "Error copying icu dat file", ex); + } + if(icuDataFile.exists()){ + icuDataFile.delete(); + } + throw new RuntimeException(ex); + } + finally { + try { + if(in != null){ + in.close(); + } + if(out != null){ + out.flush(); + out.close(); + } + } catch (IOException ioe){ + if(BuildConfig.DEBUG){ + Log.e(TAG, "Error in closing streams IO streams after expanding ICU dat file", ioe); + } + throw new RuntimeException(ioe); + } + } + } + + /** + * Implement this interface to provide custom strategy for loading jni libraries. + */ + public interface LibraryLoader { + /** + * Load jni libraries by given names. + * Straightforward implementation will be calling {@link System#loadLibrary(String name)} + * for every provided library name. + * + * @param libNames library names that sqlcipher need to load + */ + void loadLibraries(String... libNames); + } + + /** + * Loads the native SQLCipher library into the application process. + */ + public static synchronized void loadLibs (Context context) { + loadLibs(context, context.getFilesDir()); + } + + /** + * Loads the native SQLCipher library into the application process. + */ + public static synchronized void loadLibs (Context context, File workingDir) { + loadLibs(context, workingDir, new LibraryLoader() { + @Override + public void loadLibraries(String... libNames) { + for (String libName : libNames) { + System.loadLibrary(libName); + } + } + }); + } + + /** + * Loads the native SQLCipher library into the application process. + */ + public static synchronized void loadLibs(Context context, LibraryLoader libraryLoader) { + loadLibs(context, context.getFilesDir(), libraryLoader); + } + + /** + * Loads the native SQLCipher library into the application process. + */ + public static synchronized void loadLibs (Context context, File workingDir, LibraryLoader libraryLoader) { + libraryLoader.loadLibraries("sqlcipher"); + + // System.loadLibrary("stlport_shared"); + // System.loadLibrary("sqlcipher_android"); + // System.loadLibrary("database_sqlcipher"); + + // boolean systemICUFileExists = new File("/system/usr/icu/icudt46l.dat").exists(); + + // String icuRootPath = systemICUFileExists ? "/system/usr" : workingDir.getAbsolutePath(); + // setICURoot(icuRootPath); + // if(!systemICUFileExists){ + // loadICUData(context, workingDir); + // } + } + + /** + * Algorithms used in ON CONFLICT clause + * http://www.sqlite.org/lang_conflict.html + */ + /** + * When a constraint violation occurs, an immediate ROLLBACK occurs, + * thus ending the current transaction, and the command aborts with a + * return code of SQLITE_CONSTRAINT. If no transaction is active + * (other than the implied transaction that is created on every command) + * then this algorithm works the same as ABORT. + */ + public static final int CONFLICT_ROLLBACK = 1; + + /** + * When a constraint violation occurs,no ROLLBACK is executed + * so changes from prior commands within the same transaction + * are preserved. This is the default behavior. + */ + public static final int CONFLICT_ABORT = 2; + + /** + * When a constraint violation occurs, the command aborts with a return + * code SQLITE_CONSTRAINT. But any changes to the database that + * the command made prior to encountering the constraint violation + * are preserved and are not backed out. + */ + public static final int CONFLICT_FAIL = 3; + + /** + * When a constraint violation occurs, the one row that contains + * the constraint violation is not inserted or changed. + * But the command continues executing normally. Other rows before and + * after the row that contained the constraint violation continue to be + * inserted or updated normally. No error is returned. + */ + public static final int CONFLICT_IGNORE = 4; + + /** + * When a UNIQUE constraint violation occurs, the pre-existing rows that + * are causing the constraint violation are removed prior to inserting + * or updating the current row. Thus the insert or update always occurs. + * The command continues executing normally. No error is returned. + * If a NOT NULL constraint violation occurs, the NULL value is replaced + * by the default value for that column. If the column has no default + * value, then the ABORT algorithm is used. If a CHECK constraint + * violation occurs then the IGNORE algorithm is used. When this conflict + * resolution strategy deletes rows in order to satisfy a constraint, + * it does not invoke delete triggers on those rows. + * This behavior might change in a future release. + */ + public static final int CONFLICT_REPLACE = 5; + + /** + * use the following when no conflict action is specified. + */ + public static final int CONFLICT_NONE = 0; + private static final String[] CONFLICT_VALUES = new String[] + {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; + + /** + * Maximum Length Of A LIKE Or GLOB Pattern + * The pattern matching algorithm used in the default LIKE and GLOB implementation + * of SQLite can exhibit O(N^2) performance (where N is the number of characters in + * the pattern) for certain pathological cases. To avoid denial-of-service attacks + * the length of the LIKE or GLOB pattern is limited to SQLITE_MAX_LIKE_PATTERN_LENGTH bytes. + * The default value of this limit is 50000. A modern workstation can evaluate + * even a pathological LIKE or GLOB pattern of 50000 bytes relatively quickly. + * The denial of service problem only comes into play when the pattern length gets + * into millions of bytes. Nevertheless, since most useful LIKE or GLOB patterns + * are at most a few dozen bytes in length, paranoid application developers may + * want to reduce this parameter to something in the range of a few hundred + * if they know that external users are able to generate arbitrary patterns. + */ + public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000; + + /** + * Flag for {@link #openDatabase} to open the database for reading and writing. + * If the disk is full, this may fail even before you actually write anything. + * + * {@more} Note that the value of this flag is 0, so it is the default. + */ + public static final int OPEN_READWRITE = 0x00000000; // update native code if changing + + /** + * Flag for {@link #openDatabase} to open the database for reading only. + * This is the only reliable way to open a database if the disk may be full. + */ + public static final int OPEN_READONLY = 0x00000001; // update native code if changing + + private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing + + /** + * Flag for {@link #openDatabase} to open the database without support for localized collators. + * + * {@more} This causes the collator LOCALIZED not to be created. + * You must be consistent when using this flag to use the setting the database was + * created with. If this is set, {@link #setLocale} will do nothing. + */ + public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing + + /** + * Flag for {@link #openDatabase} to create the database file if it does not already exist. + */ + public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing + + /** + * SQLite memory database name + */ + public static final String MEMORY = ":memory:"; + + /** + * Indicates whether the most-recently started transaction has been marked as successful. + */ + private boolean mInnerTransactionIsSuccessful; + + /** + * Valid during the life of a transaction, and indicates whether the entire transaction (the + * outer one and all of the inner ones) so far has been successful. + */ + private boolean mTransactionIsSuccessful; + + /** + * Valid during the life of a transaction. + */ + private SQLiteTransactionListener mTransactionListener; + + /** Synchronize on this when accessing the database */ + private final ReentrantLock mLock = new ReentrantLock(true); + + private long mLockAcquiredWallTime = 0L; + private long mLockAcquiredThreadTime = 0L; + + // limit the frequency of complaints about each database to one within 20 sec + // unless run command adb shell setprop log.tag.Database VERBOSE + private static final int LOCK_WARNING_WINDOW_IN_MS = 20000; + /** If the lock is held this long then a warning will be printed when it is released. */ + private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300; + private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100; + private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000; + + private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000; + + // The pattern we remove from database filenames before + // potentially logging them. + private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); + + private long mLastLockMessageTime = 0L; + + // Things related to query logging/sampling for debugging + // slow/frequent queries during development. Always log queries + // which take (by default) 500ms+; shorter queries are sampled + // accordingly. Commit statements, which are typically slow, are + // logged together with the most recently executed SQL statement, + // for disambiguation. The 500ms value is configurable via a + // SystemProperty, but developers actively debugging database I/O + // should probably use the regular log tunable, + // LOG_SLOW_QUERIES_PROPERTY, defined below. + private static int sQueryLogTimeInMillis = 0; // lazily initialized + private static final int QUERY_LOG_SQL_LENGTH = 64; + private static final String COMMIT_SQL = "COMMIT;"; + private String mLastSqlStatement = null; + + // String prefix for slow database query EventLog records that show + // lock acquistions of the database. + /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; + + /** Used by native code, do not rename */ + /* package */ long mNativeHandle = 0; + + /** Used to make temp table names unique */ + /* package */ int mTempTableSequence = 0; + + /** The path for the database file */ + private String mPath; + + /** The anonymized path for the database file for logging purposes */ + private String mPathForLogs = null; // lazily populated + + /** The flags passed to open/create */ + private int mFlags; + + /** The optional factory to use when creating new Cursors */ + private CursorFactory mFactory; + + private WeakHashMap mPrograms; + + /** + * for each instance of this class, a cache is maintained to store + * the compiled query statement ids returned by sqlite database. + * key = sql statement with "?" for bind args + * value = {@link SQLiteCompiledSql} + * If an application opens the database and keeps it open during its entire life, then + * there will not be an overhead of compilation of sql statements by sqlite. + * + * why is this cache NOT static? because sqlite attaches compiledsql statements to the + * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is + * invoked. + * + * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method + * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because + * most of the apps don't use "?" syntax in their sql, caching is not useful for them. + */ + /* package */ Map mCompiledQueries = new HashMap(); + /** + * @hide + */ + public static final int MAX_SQL_CACHE_SIZE = 250; + private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance + private int mCacheFullWarnings; + private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1; + + /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors + * Corruption + * */ + private final DatabaseErrorHandler mErrorHandler; + + /** maintain stats about number of cache hits and misses */ + private int mNumCacheHits; + private int mNumCacheMisses; + + /** the following 2 members maintain the time when a database is opened and closed */ + private String mTimeOpened = null; + private String mTimeClosed = null; + + /** Used to find out where this object was created in case it never got closed. */ + private Throwable mStackTrace = null; + + // System property that enables logging of slow queries. Specify the threshold in ms. + private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; + private final int mSlowQueryThreshold; + + /** + * @param closable + */ + void addSQLiteClosable(SQLiteClosable closable) { + lock(); + try { + mPrograms.put(closable, null); + } finally { + unlock(); + } + } + + void removeSQLiteClosable(SQLiteClosable closable) { + lock(); + try { + mPrograms.remove(closable); + } finally { + unlock(); + } + } + + @Override + protected void onAllReferencesReleased() { + if (isOpen()) { + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeClosed = getTime(); + } + dbclose(); + + synchronized (sActiveDatabases) { + sActiveDatabases.remove(this); + } + } + } + + /** + * Attempts to release memory that SQLite holds but does not require to + * operate properly. Typically this memory will come from the page cache. + * + * @return the number of bytes actually released + */ + static public native int releaseMemory(); + + /** + * Control whether or not the SQLiteDatabase is made thread-safe by using locks + * around critical sections. This is pretty expensive, so if you know that your + * DB will only be used by a single thread then you should set this to false. + * The default is true. + * @param lockingEnabled set to true to enable locks, false otherwise + */ + public void setLockingEnabled(boolean lockingEnabled) { + mLockingEnabled = lockingEnabled; + } + + /** + * If set then the SQLiteDatabase is made thread-safe by using locks + * around critical sections + */ + private boolean mLockingEnabled = true; + + /* package */ + void onCorruption() { + if(BuildConfig.DEBUG){ + Log.e(TAG, "Calling error handler for corrupt database (detected) " + mPath); + } + + // NOTE: DefaultDatabaseErrorHandler deletes the corrupt file, EXCEPT for memory database + mErrorHandler.onCorruption(this); + } + + /** + * Locks the database for exclusive access. The database lock must be held when + * touch the native sqlite3* object since it is single threaded and uses + * a polling lock contention algorithm. The lock is recursive, and may be acquired + * multiple times by the same thread. This is a no-op if mLockingEnabled is false. + * + * @see #unlock() + */ + /* package */ void lock() { + if (!mLockingEnabled) return; + mLock.lock(); + if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { + if (mLock.getHoldCount() == 1) { + // Use elapsed real-time since the CPU may sleep when waiting for IO + mLockAcquiredWallTime = SystemClock.elapsedRealtime(); + mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); + } + } + } + + /** + * Locks the database for exclusive access. The database lock must be held when + * touch the native sqlite3* object since it is single threaded and uses + * a polling lock contention algorithm. The lock is recursive, and may be acquired + * multiple times by the same thread. + * + * @see #unlockForced() + */ + private void lockForced() { + mLock.lock(); + if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { + if (mLock.getHoldCount() == 1) { + // Use elapsed real-time since the CPU may sleep when waiting for IO + mLockAcquiredWallTime = SystemClock.elapsedRealtime(); + mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); + } + } + } + + /** + * Releases the database lock. This is a no-op if mLockingEnabled is false. + * + * @see #unlock() + */ + /* package */ void unlock() { + if (!mLockingEnabled) return; + if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { + if (mLock.getHoldCount() == 1) { + checkLockHoldTime(); + } + } + mLock.unlock(); + } + + /** + * Releases the database lock. + * + * @see #unlockForced() + */ + private void unlockForced() { + if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { + if (mLock.getHoldCount() == 1) { + checkLockHoldTime(); + } + } + mLock.unlock(); + } + + private void checkLockHoldTime() { + // Use elapsed real-time since the CPU may sleep when waiting for IO + long elapsedTime = SystemClock.elapsedRealtime(); + long lockedTime = elapsedTime - mLockAcquiredWallTime; + if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT && + !Log.isLoggable(TAG, Log.VERBOSE) && + (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) { + return; + } + if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) { + int threadTime = (int) + ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000); + if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS || + lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) { + mLastLockMessageTime = elapsedTime; + String msg = "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was " + + threadTime + "ms"; + if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) { + if(BuildConfig.DEBUG){ + Log.d(TAG, msg, new Exception()); + } + } else { + if(BuildConfig.DEBUG){ + Log.d(TAG, msg); + } + } + } + } + } + + /** + * Performs a PRAGMA integrity_check; command against the database. + * @return true if the integrity check is ok, otherwise false + */ + public boolean isDatabaseIntegrityOk() { + Pair result = getResultFromPragma("PRAGMA integrity_check;"); + return result.first ? result.second.equals("ok") : result.first; + } + + /** + * Returns a list of attached databases including the main database + * by executing PRAGMA database_list + * @return a list of pairs of database name and filename + */ + public List> getAttachedDbs() { + return getAttachedDbs(this); + } + + /** + * Sets the journal mode of the database to WAL + * @return true if successful, false otherwise + */ + public boolean enableWriteAheadLogging() { + if(inTransaction()) { + String message = "Write Ahead Logging cannot be enabled while in a transaction"; + throw new IllegalStateException(message); + } + List> attachedDbs = getAttachedDbs(this); + if(attachedDbs != null && attachedDbs.size() > 1) return false; + if(isReadOnly() || getPath().equals(MEMORY)) return false; + String command = "PRAGMA journal_mode = WAL;"; + rawExecSQL(command); + return true; + } + + /** + * Sets the journal mode of the database to DELETE (the default mode) + */ + public void disableWriteAheadLogging() { + if(inTransaction()) { + String message = "Write Ahead Logging cannot be disabled while in a transaction"; + throw new IllegalStateException(message); + } + String command = "PRAGMA journal_mode = DELETE;"; + rawExecSQL(command); + } + + /** + * @return true if the journal mode is set to WAL, otherwise false + */ + public boolean isWriteAheadLoggingEnabled() { + Pair result = getResultFromPragma("PRAGMA journal_mode;"); + return result.first ? result.second.equals("wal") : result.first; + } + + /** + * Enables or disables foreign key constraints + * @param enable used to determine whether or not foreign key constraints are on + */ + public void setForeignKeyConstraintsEnabled(boolean enable) { + if(inTransaction()) { + String message = "Foreign key constraints may not be changed while in a transaction"; + throw new IllegalStateException(message); + } + String command = String.format("PRAGMA foreign_keys = %s;", + enable ? "ON" : "OFF"); + execSQL(command); + } + + /** + * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of + * the work done in that transaction and all of the nested transactions will be committed or + * rolled back. The changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. + * + *

Here is the standard idiom for transactions: + * + *

+   *   db.beginTransaction();
+   *   try {
+   *     ...
+   *     db.setTransactionSuccessful();
+   *   } finally {
+   *     db.endTransaction();
+   *   }
+   * 
+ * + * @throws IllegalStateException if the database is not open + */ + public void beginTransaction() { + beginTransactionWithListener((SQLiteTransactionListener)null /* transactionStatusCallback */); + } + + /** + * Begins a transaction in Exlcusive mode. Transactions can be nested. When + * the outer transaction is ended all of the work done in that transaction + * and all of the nested transactions will be committed or rolled back. The + * changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they + * will be committed. + * + *

Here is the standard idiom for transactions: + * + *

+   *   db.beginTransactionWithListener(listener);
+   *   try {
+   *     ...
+   *     db.setTransactionSuccessful();
+   *   } finally {
+   *     db.endTransaction();
+   *   }
+   * 
+ * @param transactionListener listener that should be notified when the transaction begins, + * commits, or is rolled back, either explicitly or by a call to + * {@link #yieldIfContendedSafely}. + * + * @throws IllegalStateException if the database is not open + */ + public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { + beginTransactionWithListenerInternal(transactionListener, + SQLiteDatabaseTransactionType.Exclusive); + } + + /** + * Begins a transaction in Immediate mode + */ + public void beginTransactionNonExclusive() { + beginTransactionWithListenerInternal(null, + SQLiteDatabaseTransactionType.Immediate); + } + + /** + * Begins a transaction in Immediate mode + * @param transactionListener is the listener used to report transaction events + */ + public void beginTransactionWithListenerNonExclusive(SQLiteTransactionListener transactionListener) { + beginTransactionWithListenerInternal(transactionListener, + SQLiteDatabaseTransactionType.Immediate); + } + + /** + * End a transaction. See beginTransaction for notes about how to use this and when transactions + * are committed and rolled back. + * + * @throws IllegalStateException if the database is not open or is not locked by the current thread + */ + public void endTransaction() { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + if (!mLock.isHeldByCurrentThread()) { + throw new IllegalStateException("no transaction pending"); + } + try { + if (mInnerTransactionIsSuccessful) { + mInnerTransactionIsSuccessful = false; + } else { + mTransactionIsSuccessful = false; + } + if (mLock.getHoldCount() != 1) { + return; + } + RuntimeException savedException = null; + if (mTransactionListener != null) { + try { + if (mTransactionIsSuccessful) { + mTransactionListener.onCommit(); + } else { + mTransactionListener.onRollback(); + } + } catch (RuntimeException e) { + savedException = e; + mTransactionIsSuccessful = false; + } + } + if (mTransactionIsSuccessful) { + execSQL(COMMIT_SQL); + } else { + try { + execSQL("ROLLBACK;"); + if (savedException != null) { + throw savedException; + } + } catch (SQLException e) { + if(BuildConfig.DEBUG){ + Log.d(TAG, "exception during rollback, maybe the DB previously " + + "performed an auto-rollback"); + } + } + } + } finally { + mTransactionListener = null; + unlockForced(); + if(BuildConfig.DEBUG){ + Log.v(TAG, "unlocked " + Thread.currentThread() + + ", holdCount is " + mLock.getHoldCount()); + } + } + } + + /** + * Marks the current transaction as successful. Do not do any more database work between + * calling this and calling endTransaction. Do as little non-database work as possible in that + * situation too. If any errors are encountered between this and endTransaction the transaction + * will still be committed. + * + * @throws IllegalStateException if the database is not open, the current thread is not in a transaction, + * or the transaction is already marked as successful. + */ + public void setTransactionSuccessful() { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + if (!mLock.isHeldByCurrentThread()) { + throw new IllegalStateException("no transaction pending"); + } + if (mInnerTransactionIsSuccessful) { + throw new IllegalStateException( + "setTransactionSuccessful may only be called once per call to beginTransaction"); + } + mInnerTransactionIsSuccessful = true; + } + + /** + * return true if there is a transaction pending + */ + public boolean inTransaction() { + return mLock.getHoldCount() > 0; + } + + /** + * Checks if the database lock is held by this thread. + * + * @return true, if this thread is holding the database lock. + */ + public boolean isDbLockedByCurrentThread() { + return mLock.isHeldByCurrentThread(); + } + + /** + * Checks if the database is locked by another thread. This is + * just an estimate, since this status can change at any time, + * including after the call is made but before the result has + * been acted upon. + * + * @return true if the transaction was yielded, false if queue was empty or database was not open + */ + public boolean isDbLockedByOtherThreads() { + return !mLock.isHeldByCurrentThread() && mLock.isLocked(); + } + + /** + * Temporarily end the transaction to let other threads run. The transaction is assumed to be + * successful so far. Do not call setTransactionSuccessful before calling this. When this + * returns a new transaction will have been created but not marked as successful. + * + * @return true if the transaction was yielded + * + * @deprecated if the db is locked more than once (becuase of nested transactions) then the lock + * will not be yielded. Use yieldIfContendedSafely instead. + */ + @Deprecated + public boolean yieldIfContended() { + /* safeguard: */ + if (!isOpen()) return false; + + return yieldIfContendedHelper(false /* do not check yielding */, + -1 /* sleepAfterYieldDelay */); + } + + /** + * Temporarily end the transaction to let other threads run. The transaction is assumed to be + * successful so far. Do not call setTransactionSuccessful before calling this. When this + * returns a new transaction will have been created but not marked as successful. This assumes + * that there are no nested transactions (beginTransaction has only been called once) and will + * throw an exception if that is not the case. + * + * @return true if the transaction was yielded, false if queue was empty or database was not open + */ + public boolean yieldIfContendedSafely() { + /* safeguard: */ + if (!isOpen()) return false; + + return yieldIfContendedHelper(true /* check yielding */, -1 /* sleepAfterYieldDelay*/); + } + + /** + * Temporarily end the transaction to let other threads run. The transaction is assumed to be + * successful so far. Do not call setTransactionSuccessful before calling this. When this + * returns a new transaction will have been created but not marked as successful. This assumes + * that there are no nested transactions (beginTransaction has only been called once) and will + * throw an exception if that is not the case. + * + * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if + * the lock was actually yielded. This will allow other background threads to make some + * more progress than they would if we started the transaction immediately. + * + * @return true if the transaction was yielded, false if queue was empty or database was not open + * + * @throws IllegalStateException if the database is locked more than once by the current thread + * @throws InterruptedException if the thread was interrupted while sleeping + */ + public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { + /* safeguard: */ + if (!isOpen()) return false; + + return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay); + } + + private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) { + if (mLock.getQueueLength() == 0) { + // Reset the lock acquire time since we know that the thread was willing to yield + // the lock at this time. + mLockAcquiredWallTime = SystemClock.elapsedRealtime(); + mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); + return false; + } + setTransactionSuccessful(); + SQLiteTransactionListener transactionListener = mTransactionListener; + endTransaction(); + if (checkFullyYielded) { + if (this.isDbLockedByCurrentThread()) { + throw new IllegalStateException( + "Db locked more than once. yielfIfContended cannot yield"); + } + } + if (sleepAfterYieldDelay > 0) { + // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to + // check if anyone is using the database. If the database is not contended, + // retake the lock and return. + long remainingDelay = sleepAfterYieldDelay; + while (remainingDelay > 0) { + try { + Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ? + remainingDelay : SLEEP_AFTER_YIELD_QUANTUM); + } catch (InterruptedException e) { + Thread.interrupted(); + } + remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM; + if (mLock.getQueueLength() == 0) { + break; + } + } + } + beginTransactionWithListener(transactionListener); + return true; + } + + /** Maps table names to info about what to which _sync_time column to set + * to NULL on an update. This is used to support syncing. */ + private final Map mSyncUpdateInfo = + new HashMap(); + + public Map getSyncedTables() { + synchronized(mSyncUpdateInfo) { + HashMap tables = new HashMap(); + for (String table : mSyncUpdateInfo.keySet()) { + SyncUpdateInfo info = mSyncUpdateInfo.get(table); + if (info.deletedTable != null) { + tables.put(table, info.deletedTable); + } + } + return tables; + } + } + + /** + * Internal class used to keep track what needs to be marked as changed + * when an update occurs. This is used for syncing, so the sync engine + * knows what data has been updated locally. + */ + static private class SyncUpdateInfo { + /** + * Creates the SyncUpdateInfo class. + * + * @param masterTable The table to set _sync_time to NULL in + * @param deletedTable The deleted table that corresponds to the + * master table + * @param foreignKey The key that refers to the primary key in table + */ + SyncUpdateInfo(String masterTable, String deletedTable, + String foreignKey) { + this.masterTable = masterTable; + this.deletedTable = deletedTable; + this.foreignKey = foreignKey; + } + + /** The table containing the _sync_time column */ + String masterTable; + + /** The deleted table that corresponds to the master table */ + String deletedTable; + + /** The key in the local table the row in table. It may be _id, if table + * is the local table. */ + String foreignKey; + } + + /** + * Used to allow returning sub-classes of {@link Cursor} when calling query. + */ + public interface CursorFactory { + /** + * See + * {@link SQLiteCursor#SQLiteCursor(SQLiteDatabase, SQLiteCursorDriver, + * String, SQLiteQuery)}. + */ + public Cursor newCursor(SQLiteDatabase db, + SQLiteCursorDriver masterQuery, String editTable, + SQLiteQuery query); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags) { + return openDatabase(path, password, factory, flags, null); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. + * + *

Sets the locale of the database to the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file (char array) + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags) { + return openDatabase(path, password, factory, flags, null, null); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} + * with optional hook to run on pre/post key events. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options + * @param hook to run on pre/post key events + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags, SQLiteDatabaseHook hook) { + return openDatabase(path, password, factory, flags, hook, null); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} + * with optional hook to run on pre/post key events. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file (char array) + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options + * @param hook to run on pre/post key events (may be null) + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook hook) { + return openDatabase(path, password, factory, flags, hook, null); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} + * with optional hook to run on pre/post key events. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options + * @param hook to run on pre/post key events + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption (or null for default). + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags, + SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) { + return openDatabase(path, password == null ? null : password.toCharArray(), factory, flags, hook, errorHandler); + } + +/** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} + * with optional hook to run on pre/post key events. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file (char array) + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options + * @param hook to run on pre/post key events (may be null) + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption (or null for default). + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, + SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) { + byte[] keyMaterial = getBytes(password); + return openDatabase(path, keyMaterial, factory, flags, hook, errorHandler); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} + * with optional hook to run on pre/post key events. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path to database file to open and/or create + * @param password to use to open and/or create database file (byte array) + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode and other options + * @param hook to run on pre/post key events (may be null) + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption (or null for default). + * + * @return the newly opened database + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public static SQLiteDatabase openDatabase(String path, byte[] password, CursorFactory factory, int flags, + SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) { + SQLiteDatabase sqliteDatabase = null; + DatabaseErrorHandler myErrorHandler = (errorHandler != null) ? errorHandler : new DefaultDatabaseErrorHandler(); + + try { + // Open the database. + sqliteDatabase = new SQLiteDatabase(path, factory, flags, myErrorHandler); + sqliteDatabase.openDatabaseInternal(password, hook); + } catch (SQLiteDatabaseCorruptException e) { + // Try to recover from this, if possible. + // FUTURE TBD: should we consider this for other open failures? + + if(BuildConfig.DEBUG){ + Log.e(TAG, "Calling error handler for corrupt database " + path, e); + } + + // NOTE: if this errorHandler.onCorruption() throws the exception _should_ + // bubble back to the original caller. + // DefaultDatabaseErrorHandler deletes the corrupt file, EXCEPT for memory database + myErrorHandler.onCorruption(sqliteDatabase); + + // try *once* again: + sqliteDatabase = new SQLiteDatabase(path, factory, flags, myErrorHandler); + sqliteDatabase.openDatabaseInternal(password, hook); + } + + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + sqliteDatabase.enableSqlTracing(path); + } + if (SQLiteDebug.DEBUG_SQL_TIME) { + sqliteDatabase.enableSqlProfiling(path); + } + + synchronized (sActiveDatabases) { + sActiveDatabases.put(sqliteDatabase, null); + } + + return sqliteDatabase; + } + + /** + * Equivalent to openDatabase(file.getPath(), password, factory, CREATE_IF_NECESSARY, databaseHook). + */ + public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { + return openOrCreateDatabase(file, password, factory, databaseHook, null); + } + + /** + * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook). + */ + public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook, + DatabaseErrorHandler errorHandler) { + return openOrCreateDatabase(file == null ? null : file.getPath(), password, factory, databaseHook, errorHandler); + } + + /** + * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook). + */ + public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook); + } + + public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook, + DatabaseErrorHandler errorHandler) { + return openDatabase(path, password == null ? null : password.toCharArray(), factory, CREATE_IF_NECESSARY, databaseHook, errorHandler); + } + + public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook); + } + + public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook, + DatabaseErrorHandler errorHandler) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook, errorHandler); + } + + public static SQLiteDatabase openOrCreateDatabase(String path, byte[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook, null); + } + + public static SQLiteDatabase openOrCreateDatabase(String path, byte[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook, + DatabaseErrorHandler errorHandler) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook, errorHandler); + } + + /** + * Equivalent to openDatabase(file.getPath(), password, factory, CREATE_IF_NECESSARY). + */ + public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory) { + return openOrCreateDatabase(file, password, factory, null); + } + + /** + * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY). + */ + public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null); + } + + /** + * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY). + */ + public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null); + } + + /** + * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY). + */ + public static SQLiteDatabase openOrCreateDatabase(String path, byte[] password, CursorFactory factory) { + return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null, null); + } + + /** + * Create a memory backed SQLite database. Its contents will be destroyed + * when the database is closed. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called + * @param password to use to open and/or create database file + * + * @return a SQLiteDatabase object, or null if the database can't be created + * + * @throws SQLiteException if the database cannot be opened + */ + public static SQLiteDatabase create(CursorFactory factory, String password) { + // This is a magic string with special meaning for SQLite. + return openDatabase(MEMORY, password == null ? null : password.toCharArray(), factory, CREATE_IF_NECESSARY); + } + + /** + * Create a memory backed SQLite database. Its contents will be destroyed + * when the database is closed. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called + * @param password to use to open and/or create database file (char array) + * + * @return a SQLiteDatabase object, or null if the database can't be created + * + * @throws SQLiteException if the database cannot be opened + */ + public static SQLiteDatabase create(CursorFactory factory, char[] password) { + return openDatabase(MEMORY, password, factory, CREATE_IF_NECESSARY); + } + + + /** + * Close the database. + */ + public void close() { + + if (!isOpen()) { + return; // already closed + } + lock(); + try { + closeClosable(); + // close this database instance - regardless of its reference count value + onAllReferencesReleased(); + } finally { + unlock(); + } + } + + private void closeClosable() { + /* deallocate all compiled sql statement objects from mCompiledQueries cache. + * this should be done before de-referencing all {@link SQLiteClosable} objects + * from this database object because calling + * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database + * to be closed. sqlite doesn't let a database close if there are + * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries. + */ + deallocCachedSqlStatements(); + + Iterator> iter = mPrograms.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + SQLiteClosable program = entry.getKey(); + if (program != null) { + program.onAllReferencesReleasedFromContainer(); + } + } + } + + /** + * Native call to close the database. + */ + private native void dbclose(); + + /** + * Gets the database version. + * + * @return the database version + * + * @throws IllegalStateException if the database is not open + */ + public int getVersion() { + SQLiteStatement prog = null; + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + prog = new SQLiteStatement(this, "PRAGMA user_version;"); + long version = prog.simpleQueryForLong(); + return (int) version; + } finally { + if (prog != null) prog.close(); + unlock(); + } + } + + /** + * Sets the database version. + * + * @param version the new database version + * + * @throws SQLiteException if there is an issue executing the sql internally + * @throws IllegalStateException if the database is not open + */ + public void setVersion(int version) { + execSQL("PRAGMA user_version = " + version); + } + + /** + * Returns the maximum size the database may grow to. + * + * @return the new maximum database size + */ + public long getMaximumSize() { + SQLiteStatement prog = null; + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + prog = new SQLiteStatement(this, + "PRAGMA max_page_count;"); + long pageCount = prog.simpleQueryForLong(); + return pageCount * getPageSize(); + } finally { + if (prog != null) prog.close(); + unlock(); + } + } + + /** + * Sets the maximum size the database will grow to. The maximum size cannot + * be set below the current size. + * + * @param numBytes the maximum database size, in bytes + * @return the new maximum database size + */ + public long setMaximumSize(long numBytes) { + SQLiteStatement prog = null; + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + long pageSize = getPageSize(); + long numPages = numBytes / pageSize; + // If numBytes isn't a multiple of pageSize, bump up a page + if ((numBytes % pageSize) != 0) { + numPages++; + } + prog = new SQLiteStatement(this, + "PRAGMA max_page_count = " + numPages); + long newPageCount = prog.simpleQueryForLong(); + return newPageCount * pageSize; + } finally { + if (prog != null) prog.close(); + unlock(); + } + } + + /** + * Returns the current database page size, in bytes. + * + * @return the database page size, in bytes + */ + public long getPageSize() { + SQLiteStatement prog = null; + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + prog = new SQLiteStatement(this, + "PRAGMA page_size;"); + long size = prog.simpleQueryForLong(); + return size; + } finally { + if (prog != null) prog.close(); + unlock(); + } + } + + /** + * Sets the database page size. The page size must be a power of two. This + * method does not work if any data has been written to the database file, + * and must be called right after the database has been created. + * + * @param numBytes the database page size, in bytes + */ + public void setPageSize(long numBytes) { + execSQL("PRAGMA page_size = " + numBytes); + } + + /** + * Mark this table as syncable. When an update occurs in this table the +* _sync_dirty field will be set to ensure proper syncing operation. + * + * @param table the table to mark as syncable + * @param deletedTable The deleted table that corresponds to the + * syncable table + * + * @throws SQLiteException if there is an issue executing the sql to mark the table as syncable + * OR if the database is not open + * + * FUTURE @todo throw IllegalStateException if the database is not open and + * update the test suite + * + * NOTE: This method was deprecated by the AOSP in Android API 11. + */ + public void markTableSyncable(String table, String deletedTable) { + /* safeguard: */ + if (!isOpen()) { + throw new SQLiteException("database not open"); + } + + markTableSyncable(table, "_id", table, deletedTable); + } + + /** + * Mark this table as syncable, with the _sync_dirty residing in another + * table. When an update occurs in this table the _sync_dirty field of the + * row in updateTable with the _id in foreignKey will be set to + * ensure proper syncing operation. + * + * @param table an update on this table will trigger a sync time removal + * @param foreignKey this is the column in table whose value is an _id in + * updateTable + * @param updateTable this is the table that will have its _sync_dirty + * + * @throws SQLiteException if there is an issue executing the sql to mark the table as syncable + * + * FUTURE @todo throw IllegalStateException if the database is not open and + * update the test suite + * + * NOTE: This method was deprecated by the AOSP in Android API 11. + */ + public void markTableSyncable(String table, String foreignKey, + String updateTable) { + /* safeguard: */ + if (!isOpen()) { + throw new SQLiteException("database not open"); + } + + markTableSyncable(table, foreignKey, updateTable, null); + } + + /** + * Mark this table as syncable, with the _sync_dirty residing in another + * table. When an update occurs in this table the _sync_dirty field of the + * row in updateTable with the _id in foreignKey will be set to + * ensure proper syncing operation. + * + * @param table an update on this table will trigger a sync time removal + * @param foreignKey this is the column in table whose value is an _id in + * updateTable + * @param updateTable this is the table that will have its _sync_dirty + * @param deletedTable The deleted table that corresponds to the + * updateTable + * + * @throws SQLiteException if there is an issue executing the sql + */ + private void markTableSyncable(String table, String foreignKey, + String updateTable, String deletedTable) { + lock(); + try { + native_execSQL("SELECT _sync_dirty FROM " + updateTable + + " LIMIT 0"); + native_execSQL("SELECT " + foreignKey + " FROM " + table + + " LIMIT 0"); + } finally { + unlock(); + } + + SyncUpdateInfo info = new SyncUpdateInfo(updateTable, deletedTable, + foreignKey); + synchronized (mSyncUpdateInfo) { + mSyncUpdateInfo.put(table, info); + } + } + + /** + * Call for each row that is updated in a cursor. + * + * @param table the table the row is in + * @param rowId the row ID of the updated row + */ + /* package */ void rowUpdated(String table, long rowId) { + SyncUpdateInfo info; + synchronized (mSyncUpdateInfo) { + info = mSyncUpdateInfo.get(table); + } + if (info != null) { + execSQL("UPDATE " + info.masterTable + + " SET _sync_dirty=1 WHERE _id=(SELECT " + info.foreignKey + + " FROM " + table + " WHERE _id=" + rowId + ")"); + } + } + + /** + * Finds the name of the first table, which is editable. + * + * @param tables a list of tables + * @return the first table listed + */ + public static String findEditTable(String tables) { + if (!TextUtils.isEmpty(tables)) { + // find the first word terminated by either a space or a comma + int spacepos = tables.indexOf(' '); + int commapos = tables.indexOf(','); + + if (spacepos > 0 && (spacepos < commapos || commapos < 0)) { + return tables.substring(0, spacepos); + } else if (commapos > 0 && (commapos < spacepos || spacepos < 0) ) { + return tables.substring(0, commapos); + } + return tables; + } else { + throw new IllegalStateException("Invalid tables"); + } + } + + /** + * Compiles an SQL statement into a reusable pre-compiled statement object. + * The parameters are identical to {@link #execSQL(String)}. You may put ?s in the + * statement and fill in those values with {@link SQLiteProgram#bindString} + * and {@link SQLiteProgram#bindLong} each time you want to run the + * statement. Statements may not return result sets larger than 1x1. + * + * @param sql The raw SQL statement, may contain ? for unknown values to be + * bound later. + * + * @return A pre-compiled {@link SQLiteStatement} object. Note that + * {@link SQLiteStatement}s are not synchronized, see the documentation for more details. + * + * @throws SQLException If the SQL string is invalid for some reason + * @throws IllegalStateException if the database is not open + */ + public SQLiteStatement compileStatement(String sql) throws SQLException { + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + return new SQLiteStatement(this, sql); + } finally { + unlock(); + } + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid + * @throws IllegalStateException if the database is not open + * + * @see Cursor + */ + public Cursor query(boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit) { + return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, + groupBy, having, orderBy, limit); + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * @see Cursor + */ + public Cursor queryWithFactory(CursorFactory cursorFactory, + boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit) { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + String sql = SQLiteQueryBuilder.buildQueryString( + distinct, table, columns, selection, groupBy, having, orderBy, limit); + + return rawQueryWithFactory( + cursorFactory, sql, selectionArgs, findEditTable(table)); + } + + /** + * Query the given table, returning a {@link Cursor} over the result set. + * + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid + * @throws IllegalStateException if the database is not open + * + * @see Cursor + */ + public Cursor query(String table, String[] columns, String selection, + String[] selectionArgs, String groupBy, String having, + String orderBy) { + + return query(false, table, columns, selection, selectionArgs, groupBy, + having, orderBy, null /* limit */); + } + + /** + * Query the given table, returning a {@link Cursor} over the result set. + * + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid + * @throws IllegalStateException if the database is not open + * + * @see Cursor + */ + public Cursor query(String table, String[] columns, String selection, + String[] selectionArgs, String groupBy, String having, + String orderBy, String limit) { + + return query(false, table, columns, selection, selectionArgs, groupBy, + having, orderBy, limit); + } + + /** + * Runs the provided SQL and returns a {@link Cursor} over the result set. + * + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid + * @throws IllegalStateException if the database is not open + */ + public Cursor rawQuery(String sql, String[] selectionArgs) { + return rawQueryWithFactory(null, sql, selectionArgs, null); + } + + /** + * Determines the total size in bytes of the query results, and the largest + * single row in bytes for the query. + * + * @param sql the SQL query. The SQL string must a SELECT statement + * @param args the argments to bind to the query + * + * @return A {@link SQLiteQueryStats} based the provided SQL query. + */ + public SQLiteQueryStats getQueryStats(String sql, Object[] args){ + long totalPayload = 0L; + long largestIndividualPayload = 0L; + try { + String query = String.format("CREATE TABLE tempstat AS %s", sql); + execSQL(query, args); + Cursor cursor = rawQuery("SELECT sum(payload) FROM dbstat WHERE name = 'tempstat';", new Object[]{}); + if(cursor == null) return new SQLiteQueryStats(totalPayload, largestIndividualPayload); + cursor.moveToFirst(); + totalPayload = cursor.getLong(0); + cursor.close(); + cursor = rawQuery("SELECT max(mx_payload) FROM dbstat WHERE name = 'tempstat';", new Object[]{}); + if(cursor == null) return new SQLiteQueryStats(totalPayload, largestIndividualPayload); + cursor.moveToFirst(); + largestIndividualPayload = cursor.getLong(0); + cursor.close(); + execSQL("DROP TABLE tempstat;"); + } catch(SQLiteException ex) { + execSQL("DROP TABLE IF EXISTS tempstat;"); + throw ex; + } + return new SQLiteQueryStats(totalPayload, largestIndividualPayload); + } + + /** + * Runs the provided SQL and returns a {@link Cursor} over the result set. + * + * @param sql the SQL query. The SQL string must not be ; terminated + * @param args You may include ?s in where clause in the query, + * which will be replaced by the values from args. The + * values will be bound by their type. + * + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid + * @throws IllegalStateException if the database is not open + */ + public Cursor rawQuery(String sql, Object[] args) { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + long timeStart = 0; + if (Config.LOGV || mSlowQueryThreshold != -1) { + timeStart = System.currentTimeMillis(); + } + SQLiteDirectCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, null); + Cursor cursor = null; + try { + cursor = driver.query(mFactory, args); + } finally { + if (Config.LOGV || mSlowQueryThreshold != -1) { + // Force query execution + int count = -1; + if (cursor != null) { + count = cursor.getCount(); + } + + long duration = System.currentTimeMillis() - timeStart; + + if (BuildConfig.DEBUG || duration >= mSlowQueryThreshold) { + Log.v(TAG, + "query (" + duration + " ms): " + driver.toString() + + ", args are , count is " + count); + } + } + } + return new CrossProcessCursorWrapper(cursor); + } + + /** + * Runs the provided SQL and returns a cursor over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * @param editTable the name of the first table, which is editable + * + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid + * @throws IllegalStateException if the database is not open + */ + public Cursor rawQueryWithFactory( + CursorFactory cursorFactory, String sql, String[] selectionArgs, + String editTable) { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + long timeStart = 0; + + if (Config.LOGV || mSlowQueryThreshold != -1) { + timeStart = System.currentTimeMillis(); + } + + SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable); + + Cursor cursor = null; + try { + cursor = driver.query( + cursorFactory != null ? cursorFactory : mFactory, + selectionArgs); + } finally { + if (Config.LOGV || mSlowQueryThreshold != -1) { + + // Force query execution + int count = -1; + if (cursor != null) { + count = cursor.getCount(); + } + + long duration = System.currentTimeMillis() - timeStart; + + if (BuildConfig.DEBUG || duration >= mSlowQueryThreshold) { + Log.v(TAG, + "query (" + duration + " ms): " + driver.toString() + + ", args are , count is " + count); + } + } + } + return new CrossProcessCursorWrapper(cursor); + } + + /** + * Runs the provided SQL and returns a cursor over the result set. + * The cursor will read an initial set of rows and the return to the caller. + * It will continue to read in batches and send data changed notifications + * when the later batches are ready. + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * @param initialRead set the initial count of items to read from the cursor + * @param maxRead set the count of items to read on each iteration after the first + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * This work is incomplete and not fully tested or reviewed, so currently + * hidden. + * @hide + */ + public Cursor rawQuery(String sql, String[] selectionArgs, + int initialRead, int maxRead) { + net.sqlcipher.CursorWrapper cursorWrapper = (net.sqlcipher.CursorWrapper)rawQueryWithFactory(null, sql, selectionArgs, null); + ((SQLiteCursor)cursorWrapper.getWrappedCursor()).setLoadStyle(initialRead, maxRead); + return cursorWrapper; + } + + /** + * Convenience method for inserting a row into the database. + * + * @param table the table to insert the row into + * @param nullColumnHack SQL doesn't allow inserting a completely empty row, + * so if initialValues is empty this column will explicitly be + * assigned a NULL value + * @param values this map contains the initial column values for the + * row. The keys should be the column names and the values the + * column values + * @return the row ID of the newly inserted row, or -1 if an error occurred + */ + public long insert(String table, String nullColumnHack, ContentValues values) { + try { + return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); + } catch (SQLException e) { + if(BuildConfig.DEBUG){ + Log.e(TAG, "Error inserting into " + table, e); + } + return -1; + } + } + + /** + * Convenience method for inserting a row into the database. + * + * @param table the table to insert the row into + * @param nullColumnHack SQL doesn't allow inserting a completely empty row, + * so if initialValues is empty this column will explicitly be + * assigned a NULL value + * @param values this map contains the initial column values for the + * row. The keys should be the column names and the values the + * column values + * @throws SQLException + * @return the row ID of the newly inserted row, or -1 if an error occurred + */ + public long insertOrThrow(String table, String nullColumnHack, ContentValues values) + throws SQLException { + return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); + } + + /** + * Convenience method for replacing a row in the database. + * + * @param table the table in which to replace the row + * @param nullColumnHack SQL doesn't allow inserting a completely empty row, + * so if initialValues is empty this row will explicitly be + * assigned a NULL value + * @param initialValues this map contains the initial column values for + * the row. The key + * @return the row ID of the newly inserted row, or -1 if an error occurred + */ + public long replace(String table, String nullColumnHack, ContentValues initialValues) { + try { + return insertWithOnConflict(table, nullColumnHack, initialValues, + CONFLICT_REPLACE); + } catch (SQLException e) { + if(BuildConfig.DEBUG){ + Log.e(TAG, "Error inserting into " + table, e); + } + return -1; + } + } + + /** + * Convenience method for replacing a row in the database. + * + * @param table the table in which to replace the row + * @param nullColumnHack SQL doesn't allow inserting a completely empty row, + * so if initialValues is empty this row will explicitly be + * assigned a NULL value + * @param initialValues this map contains the initial column values for + * the row. The key + * @throws SQLException + * @return the row ID of the newly inserted row, or -1 if an error occurred + */ + public long replaceOrThrow(String table, String nullColumnHack, + ContentValues initialValues) throws SQLException { + return insertWithOnConflict(table, nullColumnHack, initialValues, + CONFLICT_REPLACE); + } + + /** + * General method for inserting a row into the database. + * + * @param table the table to insert the row into + * @param nullColumnHack SQL doesn't allow inserting a completely empty row, + * so if initialValues is empty this column will explicitly be + * assigned a NULL value + * @param initialValues this map contains the initial column values for the + * row. The keys should be the column names and the values the + * column values + * @param conflictAlgorithm for insert conflict resolver + * + * @return the row ID of the newly inserted row + * OR the primary key of the existing row if the input param 'conflictAlgorithm' = + * {@link #CONFLICT_IGNORE} + * OR -1 if any error + * + * @throws SQLException If the SQL string is invalid for some reason + * @throws IllegalStateException if the database is not open + */ + public long insertWithOnConflict(String table, String nullColumnHack, + ContentValues initialValues, int conflictAlgorithm) { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + + // Measurements show most sql lengths <= 152 + StringBuilder sql = new StringBuilder(152); + sql.append("INSERT"); + sql.append(CONFLICT_VALUES[conflictAlgorithm]); + sql.append(" INTO "); + sql.append(table); + // Measurements show most values lengths < 40 + StringBuilder values = new StringBuilder(40); + + Set> entrySet = null; + if (initialValues != null && initialValues.size() > 0) { + entrySet = initialValues.valueSet(); + Iterator> entriesIter = entrySet.iterator(); + sql.append('('); + + boolean needSeparator = false; + while (entriesIter.hasNext()) { + if (needSeparator) { + sql.append(", "); + values.append(", "); + } + needSeparator = true; + Map.Entry entry = entriesIter.next(); + sql.append(entry.getKey()); + values.append('?'); + } + + sql.append(')'); + } else { + sql.append("(" + nullColumnHack + ") "); + values.append("NULL"); + } + + sql.append(" VALUES("); + sql.append(values); + sql.append(");"); + + lock(); + SQLiteStatement statement = null; + try { + statement = compileStatement(sql.toString()); + + // Bind the values + if (entrySet != null) { + int size = entrySet.size(); + Iterator> entriesIter = entrySet.iterator(); + for (int i = 0; i < size; i++) { + Map.Entry entry = entriesIter.next(); + DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue()); + + } + } + + // Run the program and then cleanup + statement.execute(); + + long insertedRowId = lastChangeCount() > 0 ? lastInsertRow() : -1; + if (insertedRowId == -1) { + if(BuildConfig.DEBUG){ + Log.e(TAG, "Error inserting using into " + table); + } + } else { + if (BuildConfig.DEBUG && Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Inserting row " + insertedRowId + + " from using into " + table); + } + } + return insertedRowId; + } catch (SQLiteDatabaseCorruptException e) { + onCorruption(); + throw e; + } finally { + if (statement != null) { + statement.close(); + } + unlock(); + } + } + + /** + * Convenience method for deleting rows in the database. + * + * @param table the table to delete from + * @param whereClause the optional WHERE clause to apply when deleting. + * Passing null will delete all rows. + * + * @return the number of rows affected if a whereClause is passed in, 0 + * otherwise. To remove all rows and get a count pass "1" as the + * whereClause. + * + * @throws SQLException If the SQL string is invalid for some reason + * @throws IllegalStateException if the database is not open + */ + public int delete(String table, String whereClause, String[] whereArgs) { + return delete(table, whereClause, (Object[])whereArgs); + } + + /** + * Convenience method for deleting rows in the database. + * + * @param table the table to delete from + * @param whereClause the optional WHERE clause to apply when deleting. + * Passing null will delete all rows. + * + * @return the number of rows affected if a whereClause is passed in, 0 + * otherwise. To remove all rows and get a count pass "1" as the + * whereClause. + * + * @throws SQLException If the SQL string is invalid for some reason + * @throws IllegalStateException if the database is not open + */ + public int delete(String table, String whereClause, Object[] whereArgs) { + SQLiteStatement statement = null; + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + statement = compileStatement("DELETE FROM " + table + + (!TextUtils.isEmpty(whereClause) + ? " WHERE " + whereClause : "")); + if (whereArgs != null) { + int numArgs = whereArgs.length; + for (int i = 0; i < numArgs; i++) { + DatabaseUtils.bindObjectToProgram(statement, i + 1, whereArgs[i]); + } + } + statement.execute(); + return lastChangeCount(); + } catch (SQLiteDatabaseCorruptException e) { + onCorruption(); + throw e; + } finally { + if (statement != null) { + statement.close(); + } + unlock(); + } + } + + /** + * Convenience method for updating rows in the database. + * + * @param table the table to update in + * @param values a map from column names to new column values. null is a + * valid value that will be translated to NULL. + * @param whereClause the optional WHERE clause to apply when updating. + * Passing null will update all rows. + * + * @return the number of rows affected + * + * @throws SQLException If the SQL string is invalid for some reason + * @throws IllegalStateException if the database is not open + */ + public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { + return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE); + } + + /** + * Convenience method for updating rows in the database. + * + * @param table the table to update in + * @param values a map from column names to new column values. null is a + * valid value that will be translated to NULL. + * @param whereClause the optional WHERE clause to apply when updating. + * Passing null will update all rows. + * @param conflictAlgorithm for update conflict resolver + * + * @return the number of rows affected + * + * @throws SQLException If the SQL string is invalid for some reason + * @throws IllegalStateException if the database is not open + */ + public int updateWithOnConflict(String table, ContentValues values, + String whereClause, String[] whereArgs, int conflictAlgorithm) { + if (values == null || values.size() == 0) { + throw new IllegalArgumentException("Empty values"); + } + + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(CONFLICT_VALUES[conflictAlgorithm]); + sql.append(table); + sql.append(" SET "); + + Set> entrySet = values.valueSet(); + Iterator> entriesIter = entrySet.iterator(); + + while (entriesIter.hasNext()) { + Map.Entry entry = entriesIter.next(); + sql.append(entry.getKey()); + sql.append("=?"); + if (entriesIter.hasNext()) { + sql.append(", "); + } + } + + if (!TextUtils.isEmpty(whereClause)) { + sql.append(" WHERE "); + sql.append(whereClause); + } + SQLiteStatement statement = null; + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + statement = compileStatement(sql.toString()); + + // Bind the values + int size = entrySet.size(); + entriesIter = entrySet.iterator(); + int bindArg = 1; + for (int i = 0; i < size; i++) { + Map.Entry entry = entriesIter.next(); + DatabaseUtils.bindObjectToProgram(statement, bindArg, entry.getValue()); + bindArg++; + } + + if (whereArgs != null) { + size = whereArgs.length; + for (int i = 0; i < size; i++) { + statement.bindString(bindArg, whereArgs[i]); + bindArg++; + } + } + + // Run the program and then cleanup + statement.execute(); + int numChangedRows = lastChangeCount(); + if (BuildConfig.DEBUG && Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Updated " + numChangedRows + + " rows using and for " + table); + } + return numChangedRows; + } catch (SQLiteDatabaseCorruptException e) { + onCorruption(); + throw e; + } catch (SQLException e) { + if(BuildConfig.DEBUG){ + Log.e(TAG, "Error updating using for " + table); + } + throw e; + } finally { + if (statement != null) { + statement.close(); + } + unlock(); + } + } + + /** + * Execute a single SQL statement that is not a query. For example, CREATE + * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not + * supported. it takes a write lock + * + * @throws SQLException If the SQL string is invalid for some reason + * @throws IllegalStateException if the database is not open + */ + public void execSQL(String sql) throws SQLException { + long timeStart = SystemClock.uptimeMillis(); + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + native_execSQL(sql); + } catch (SQLiteDatabaseCorruptException e) { + onCorruption(); + throw e; + } finally { + unlock(); + } + } + + public void rawExecSQL(String sql){ + long timeStart = SystemClock.uptimeMillis(); + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + native_rawExecSQL(sql); + } catch (SQLiteDatabaseCorruptException e) { + onCorruption(); + throw e; + } finally { + unlock(); + } + } + + /** + * Execute a single SQL statement that is not a query. For example, CREATE + * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not + * supported. it takes a write lock, + * + * @param sql + * @param bindArgs only byte[], String, Long and Double are supported in bindArgs. + * + * @throws SQLException If the SQL string is invalid for some reason + * @throws IllegalStateException if the database is not open + */ + public void execSQL(String sql, Object[] bindArgs) throws SQLException { + SQLiteStatement statement = null; + if (bindArgs == null) { + throw new IllegalArgumentException("Empty bindArgs"); + } + long timeStart = SystemClock.uptimeMillis(); + lock(); + try { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + statement = compileStatement(sql); + if (bindArgs != null) { + int numArgs = bindArgs.length; + for (int i = 0; i < numArgs; i++) { + DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]); + } + } + statement.execute(); + } catch (SQLiteDatabaseCorruptException e) { + onCorruption(); + throw e; + } finally { + if (statement != null) { + statement.close(); + } + unlock(); + } + } + + @Override + protected void finalize() { + if (isOpen()) { + if(BuildConfig.DEBUG){ + Log.e(TAG, "close() was never explicitly called on database '" + + mPath + "' ", mStackTrace); + } + closeClosable(); + onAllReferencesReleased(); + } + } + + /** + * Public constructor which attempts to open the database. See {@link #create} and {@link #openDatabase}. + * + *

Sets the locale of the database to the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path The full path to the database + * @param password to use to open and/or create a database file (char array) + * @param factory The factory to use when creating cursors, may be NULL. + * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already + * exists, mFlags will be updated appropriately. + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags) { + this(path, factory, flags, null); + this.openDatabaseInternal(password, null); + } + + /** + * Public constructor which attempts to open the database. See {@link #create} and {@link #openDatabase}. + * + *

Sets the locale of the database to the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path The full path to the database + * @param password to use to open and/or create a database file (char array) + * @param factory The factory to use when creating cursors, may be NULL. + * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already + * exists, mFlags will be updated appropriately. + * @param databaseHook to run on pre/post key events + * + * @throws SQLiteException if the database cannot be opened + * @throws IllegalArgumentException if the database path is null + */ + public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) { + this(path, factory, flags, null); + this.openDatabaseInternal(password, databaseHook); + } + + public SQLiteDatabase(String path, byte[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) { + this(path, factory, flags, null); + this.openDatabaseInternal(password, databaseHook); + } + + /** + * Private constructor (without database password) which DOES NOT attempt to open the database. + * + * @param path The full path to the database + * @param factory The factory to use when creating cursors, may be NULL. + * @param flags to control database access mode and other options + * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption (or null for default). + * + * @throws IllegalArgumentException if the database path is null + */ + private SQLiteDatabase(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) { + if (path == null) { + throw new IllegalArgumentException("path should not be null"); + } + + mFlags = flags; + mPath = path; + + mSlowQueryThreshold = -1;//SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); + mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); + mFactory = factory; + mPrograms = new WeakHashMap(); + + mErrorHandler = errorHandler; + } + + private void openDatabaseInternal(final char[] password, SQLiteDatabaseHook hook) { + final byte[] keyMaterial = getBytes(password); + openDatabaseInternal(keyMaterial, hook); + } + + private void openDatabaseInternal(final byte[] password, SQLiteDatabaseHook hook) { + boolean shouldCloseConnection = true; + dbopen(mPath, mFlags); + try { + keyDatabase(hook, new Runnable() { + public void run() { + if(password != null && password.length > 0) { + key(password); + } + } + }); + shouldCloseConnection = false; + + } catch(RuntimeException ex) { + + final char[] keyMaterial = getChars(password); + if(containsNull(keyMaterial)) { + keyDatabase(hook, new Runnable() { + public void run() { + if(password != null) { + key_mutf8(keyMaterial); + } + } + }); + if(password != null && password.length > 0) { + rekey(password); + } + shouldCloseConnection = false; + } else { + throw ex; + } + if(keyMaterial != null && keyMaterial.length > 0) { + Arrays.fill(keyMaterial, (char)0); + } + + } finally { + if(shouldCloseConnection) { + dbclose(); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeClosed = getTime(); + } + } + } + + } + + private boolean containsNull(char[] data) { + char defaultValue = '\u0000'; + boolean status = false; + if(data != null && data.length > 0) { + for(char datum : data) { + if(datum == defaultValue) { + status = true; + break; + } + } + } + return status; + } + + private void keyDatabase(SQLiteDatabaseHook databaseHook, Runnable keyOperation) { + if(databaseHook != null) { + databaseHook.preKey(this); + } + if(keyOperation != null){ + keyOperation.run(); + } + if(databaseHook != null){ + databaseHook.postKey(this); + } + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeOpened = getTime(); + } + try { + Cursor cursor = rawQuery("select count(*) from sqlite_master;", new String[]{}); + if(cursor != null){ + cursor.moveToFirst(); + int count = cursor.getInt(0); + cursor.close(); + } + } catch (RuntimeException e) { + if(BuildConfig.DEBUG){ + Log.e(TAG, e.getMessage(), e); + } + throw e; + } + } + + private String getTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ", Locale.US).format(System.currentTimeMillis()); + } + + /** + * return whether the DB is opened as read only. + * @return true if DB is opened as read only + */ + public boolean isReadOnly() { + return (mFlags & OPEN_READ_MASK) == OPEN_READONLY; + } + + /** + * @return true if the DB is currently open (has not been closed) + */ + public boolean isOpen() { + return mNativeHandle != 0; + } + + public boolean needUpgrade(int newVersion) { + /* NOTE: getVersion() will throw if database is not open. */ + return newVersion > getVersion(); + } + + /** + * Getter for the path to the database file. + * + * @return the path to our database file. + */ + public final String getPath() { + return mPath; + } + + /** + * Removes email addresses from database filenames before they're + * logged to the EventLog where otherwise apps could potentially + * read them. + */ + private String getPathForLogs() { + if (mPathForLogs != null) { + return mPathForLogs; + } + if (mPath == null) { + return null; + } + if (mPath.indexOf('@') == -1) { + mPathForLogs = mPath; + } else { + mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY"); + } + return mPathForLogs; + } + + /** + * Sets the locale for this database. Does nothing if this database has + * the NO_LOCALIZED_COLLATORS flag set or was opened read only. + * + * @throws SQLException if the locale could not be set. The most common reason + * for this is that there is no collator available for the locale you requested. + * In this case the database remains unchanged. + */ + public void setLocale(Locale locale) { + lock(); + try { + native_setLocale(locale.toString(), mFlags); + } finally { + unlock(); + } + } + + /* + * ============================================================================ + * + * The following methods deal with compiled-sql cache + * ============================================================================ + */ + /** + * adds the given sql and its compiled-statement-id-returned-by-sqlite to the + * cache of compiledQueries attached to 'this'. + * + * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql, + * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current + * mapping is NOT replaced with the new mapping). + */ + /* package */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) { + if (mMaxSqlCacheSize == 0) { + // for this database, there is no cache of compiled sql. + if (SQLiteDebug.DEBUG_SQL_CACHE && BuildConfig.DEBUG) { + Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql); + } + return; + } + + SQLiteCompiledSql compiledSql = null; + synchronized(mCompiledQueries) { + // don't insert the new mapping if a mapping already exists + compiledSql = mCompiledQueries.get(sql); + if (compiledSql != null) { + return; + } + // add this to the cache + if (mCompiledQueries.size() == mMaxSqlCacheSize) { + /* + * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. + * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times + * chances are it is NOT using ? for bindargs - so caching is useless. + * TODO: either let the callers set max cchesize for their app, or intelligently + * figure out what should be cached for a given app. + */ + if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION && BuildConfig.DEBUG) { + Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + + getPath() + "; i.e., NO space for this sql statement in cache: " + + sql + ". Please change your sql statements to use '?' for " + + "bindargs, instead of using actual values"); + } + // don't add this entry to cache + } else { + // cache is NOT full. add this to cache. + mCompiledQueries.put(sql, compiledStatement); + if (SQLiteDebug.DEBUG_SQL_CACHE && BuildConfig.DEBUG) { + Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + + mCompiledQueries.size() + "|" + sql); + } + } + } + return; + } + + + private void deallocCachedSqlStatements() { + synchronized (mCompiledQueries) { + for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) { + compiledSql.releaseSqlStatement(); + } + mCompiledQueries.clear(); + } + } + + /** + * from the compiledQueries cache, returns the compiled-statement-id for the given sql. + * returns null, if not found in the cache. + */ + /* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) { + SQLiteCompiledSql compiledStatement = null; + boolean cacheHit; + synchronized(mCompiledQueries) { + if (mMaxSqlCacheSize == 0) { + // for this database, there is no cache of compiled sql. + if (SQLiteDebug.DEBUG_SQL_CACHE && BuildConfig.DEBUG) { + Log.v(TAG, "|cache NOT found|" + getPath()); + } + return null; + } + cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null; + } + if (cacheHit) { + mNumCacheHits++; + } else { + mNumCacheMisses++; + } + + if (SQLiteDebug.DEBUG_SQL_CACHE && BuildConfig.DEBUG) { + Log.v(TAG, "|cache_stats|" + + getPath() + "|" + mCompiledQueries.size() + + "|" + mNumCacheHits + "|" + mNumCacheMisses + + "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql); + } + return compiledStatement; + } + + /** + * returns true if the given sql is cached in compiled-sql cache. + * @hide + */ + public boolean isInCompiledSqlCache(String sql) { + synchronized(mCompiledQueries) { + return mCompiledQueries.containsKey(sql); + } + } + + /** + * purges the given sql from the compiled-sql cache. + * @hide + */ + public void purgeFromCompiledSqlCache(String sql) { + synchronized(mCompiledQueries) { + mCompiledQueries.remove(sql); + } + } + + /** + * remove everything from the compiled sql cache + * @hide + */ + public void resetCompiledSqlCache() { + deallocCachedSqlStatements(); + } + + /** + * return the current maxCacheSqlCacheSize + * @hide + */ + public synchronized int getMaxSqlCacheSize() { + return mMaxSqlCacheSize; + } + + /** + * set the max size of the compiled sql cache for this database after purging the cache. + * (size of the cache = number of compiled-sql-statements stored in the cache). + * + * max cache size can ONLY be increased from its current size (default = 0). + * if this method is called with smaller size than the current value of mMaxSqlCacheSize, + * then IllegalStateException is thrown + * + * synchronized because we don't want t threads to change cache size at the same time. + * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE) + * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or + * < the value set with previous setMaxSqlCacheSize() call. + * + * @hide + */ + public synchronized void setMaxSqlCacheSize(int cacheSize) { + if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { + throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE); + } else if (cacheSize < mMaxSqlCacheSize) { + throw new IllegalStateException("cannot set cacheSize to a value less than the value " + + "set with previous setMaxSqlCacheSize() call."); + } + mMaxSqlCacheSize = cacheSize; + } + + public static byte[] getBytes(char[] data) { + if(data == null || data.length == 0) return null; + CharBuffer charBuffer = CharBuffer.wrap(data); + ByteBuffer byteBuffer = Charset.forName(KEY_ENCODING).encode(charBuffer); + byte[] result = new byte[byteBuffer.limit()]; + byteBuffer.get(result); + return result; + } + + public static char[] getChars(byte[] data){ + if(data == null || data.length == 0) return null; + ByteBuffer byteBuffer = ByteBuffer.wrap(data); + CharBuffer charBuffer = Charset.forName(KEY_ENCODING).decode(byteBuffer); + char[] result = new char[charBuffer.limit()]; + charBuffer.get(result); + return result; + } + + /* begin SQLiteSupportDatabase methods */ + + @Override + public android.database.Cursor query(String query) { + return rawQuery(query, null); + } + + @Override + public android.database.Cursor query(String query, Object[] bindArgs) { + return rawQuery(query, bindArgs); + } + + @Override + public android.database.Cursor query(SupportSQLiteQuery query) { + return query(query, null); + } + + @Override + public android.database.Cursor query(final SupportSQLiteQuery supportQuery, + CancellationSignal cancellationSignal) { + String sql = supportQuery.getSql(); + int argumentCount = supportQuery.getArgCount(); + Object[] args = new Object[argumentCount]; + SQLiteDirectCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, null); + SQLiteQuery query = new SQLiteQuery(this, sql, 0, args); + supportQuery.bindTo(query); + return new CrossProcessCursorWrapper(new SQLiteCursor(this, driver, null, query)); + } + + @Override + public long insert(String table, int conflictAlgorithm, + ContentValues values) + throws android.database.SQLException { + return insertWithOnConflict(table, null, values, conflictAlgorithm); + } + + @Override + public int update(String table, int conflictAlgorithm, ContentValues values, + String whereClause, Object[] whereArgs) { + int whereArgsLength = whereArgs == null + ? 0 + : whereArgs.length; + String[] args = new String[whereArgsLength]; + for (int i = 0; i < whereArgsLength; i++) { + args[i] = whereArgs[i].toString(); + } + return updateWithOnConflict(table, values, whereClause, args, conflictAlgorithm); + } + + @Override + public void beginTransactionWithListener( + final android.database.sqlite.SQLiteTransactionListener transactionListener) { + beginTransactionWithListener(new SQLiteTransactionListener() { + @Override + public void onBegin() { + transactionListener.onBegin(); + } + + @Override + public void onCommit() { + transactionListener.onCommit(); + } + + @Override + public void onRollback() { + transactionListener.onRollback(); + } + }); + } + + @Override + public void beginTransactionWithListenerNonExclusive( + final android.database.sqlite.SQLiteTransactionListener transactionListener) { + beginTransactionWithListenerNonExclusive( + new SQLiteTransactionListener() { + @Override + public void onBegin() { + transactionListener.onBegin(); + } + + @Override + public void onCommit() { + transactionListener.onCommit(); + } + + @Override + public void onRollback() { + transactionListener.onRollback(); + } + }); + } + + /* end SQLiteSupportDatabase methods */ + + private void beginTransactionWithListenerInternal(SQLiteTransactionListener transactionListener, + SQLiteDatabaseTransactionType transactionType) { + lockForced(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + boolean ok = false; + try { + // If this thread already had the lock then get out + if (mLock.getHoldCount() > 1) { + if (mInnerTransactionIsSuccessful) { + String msg = "Cannot call beginTransaction between " + + "calling setTransactionSuccessful and endTransaction"; + IllegalStateException e = new IllegalStateException(msg); + if(BuildConfig.DEBUG){ + Log.e(TAG, "beginTransaction() failed", e); + } + throw e; + } + ok = true; + return; + } + // This thread didn't already have the lock, so begin a database + // transaction now. + if(transactionType == SQLiteDatabaseTransactionType.Exclusive) { + execSQL("BEGIN EXCLUSIVE;"); + } else if(transactionType == SQLiteDatabaseTransactionType.Immediate) { + execSQL("BEGIN IMMEDIATE;"); + } else if(transactionType == SQLiteDatabaseTransactionType.Deferred) { + execSQL("BEGIN DEFERRED;"); + } else { + String message = String.format("%s is an unsupported transaction type", + transactionType); + throw new IllegalArgumentException(message); + } + mTransactionListener = transactionListener; + mTransactionIsSuccessful = true; + mInnerTransactionIsSuccessful = false; + if (transactionListener != null) { + try { + transactionListener.onBegin(); + } catch (RuntimeException e) { + execSQL("ROLLBACK;"); + throw e; + } + } + ok = true; + } finally { + if (!ok) { + // beginTransaction is called before the try block so we must release the lock in + // the case of failure. + unlockForced(); + } + } + } + + /** + * this method is used to collect data about ALL open databases in the current process. + * bugreport is a user of this data. + */ + /* package */ static ArrayList getDbStats() { + ArrayList dbStatsList = new ArrayList(); + + for (SQLiteDatabase db : getActiveDatabases()) { + if (db == null || !db.isOpen()) { + continue; + } + + // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db + int lookasideUsed = db.native_getDbLookaside(); + + // get the lastnode of the dbname + String path = db.getPath(); + int indx = path.lastIndexOf("/"); + String lastnode = path.substring((indx != -1) ? ++indx : 0); + + // get list of attached dbs and for each db, get its size and pagesize + ArrayList> attachedDbs = getAttachedDbs(db); + if (attachedDbs == null) { + continue; + } + for (int i = 0; i < attachedDbs.size(); i++) { + Pair p = attachedDbs.get(i); + long pageCount = getPragmaVal(db, p.first + ".page_count;"); + + // first entry in the attached db list is always the main database + // don't worry about prefixing the dbname with "main" + String dbName; + if (i == 0) { + dbName = lastnode; + } else { + // lookaside is only relevant for the main db + lookasideUsed = 0; + dbName = " (attached) " + p.first; + // if the attached db has a path, attach the lastnode from the path to above + if (p.second.trim().length() > 0) { + int idx = p.second.lastIndexOf("/"); + dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0); + } + } + if (pageCount > 0) { + dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), + lookasideUsed)); + } + } + } + return dbStatsList; + } + + private static ArrayList getActiveDatabases() { + ArrayList databases = new ArrayList(); + synchronized (sActiveDatabases) { + databases.addAll(sActiveDatabases.keySet()); + } + return databases; + } + + /** + * get the specified pragma value from sqlite for the specified database. + * only handles pragma's that return int/long. + * NO JAVA locks are held in this method. + * TODO: use this to do all pragma's in this class + */ + private static long getPragmaVal(SQLiteDatabase db, String pragma) { + if (!db.isOpen()) { + return 0; + } + SQLiteStatement prog = null; + try { + prog = new SQLiteStatement(db, "PRAGMA " + pragma); + long val = prog.simpleQueryForLong(); + return val; + } finally { + if (prog != null) prog.close(); + } + } + + /** + * returns list of full pathnames of all attached databases + * including the main database + * TODO: move this to {@link DatabaseUtils} + */ + private static ArrayList> getAttachedDbs(SQLiteDatabase dbObj) { + if (!dbObj.isOpen()) { + return null; + } + ArrayList> attachedDbs = new ArrayList>(); + Cursor c = dbObj.rawQuery("pragma database_list;", null); + while (c.moveToNext()) { + attachedDbs.add(new Pair(c.getString(1), c.getString(2))); + } + c.close(); + return attachedDbs; + } + + private Pair getResultFromPragma(String command) { + Pair result = new Pair(false, ""); + Cursor cursor = rawQuery(command, new Object[]{}); + if(cursor == null) return result; + if(cursor.moveToFirst()){ + String value = cursor.getString(0); + result = new Pair(true, value); + } + cursor.close(); + return result; + } + + + /** + * Sets the root directory to search for the ICU data file + */ + public static native void setICURoot(String path); + + /** + * Native call to open the database. + * + * @param path The full path to the database + */ + private native void dbopen(String path, int flags); + + /** + * Native call to setup tracing of all sql statements + * + * @param path the full path to the database + */ + private native void enableSqlTracing(String path); + + /** + * Native call to setup profiling of all sql statements. + * currently, sqlite's profiling = printing of execution-time + * (wall-clock time) of each of the sql statements, as they + * are executed. + * + * @param path the full path to the database + */ + private native void enableSqlProfiling(String path); + + /** + * Native call to execute a raw SQL statement. {@link #lock} must be held + * when calling this method. + * + * @param sql The raw SQL string + * + * @throws SQLException + */ + /* package */ native void native_execSQL(String sql) throws SQLException; + + /** + * Native call to set the locale. {@link #lock} must be held when calling + * this method. + * + * @throws SQLException + */ + /* package */ native void native_setLocale(String loc, int flags); + + /** + * Returns the row ID of the last row inserted into the database. + * + * @return the row ID of the last row inserted into the database. + */ + /* package */ native long lastInsertRow(); + + /** + * Returns the number of changes made in the last statement executed. + * + * @return the number of changes made in the last statement executed. + */ + /* package */ native int lastChangeCount(); + + /** + * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here + * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html + * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED + */ + private native int native_getDbLookaside(); + + private native void native_rawExecSQL(String sql); + + private native int native_status(int operation, boolean reset); + + private native void key(byte[] key) throws SQLException; + private native void key_mutf8(char[] key) throws SQLException; + private native void rekey(byte[] key) throws SQLException; +} diff --git a/src/net/sqlcipher/database/SQLiteDatabaseHook.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDatabaseHook.java similarity index 100% rename from src/net/sqlcipher/database/SQLiteDatabaseHook.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDatabaseHook.java diff --git a/src/net/sqlcipher/database/SQLiteDebug.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDebug.java similarity index 100% rename from src/net/sqlcipher/database/SQLiteDebug.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDebug.java diff --git a/src/net/sqlcipher/database/SQLiteDirectCursorDriver.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDirectCursorDriver.java similarity index 98% rename from src/net/sqlcipher/database/SQLiteDirectCursorDriver.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDirectCursorDriver.java index 37ef5a5e..36ae59a4 100644 --- a/src/net/sqlcipher/database/SQLiteDirectCursorDriver.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteDirectCursorDriver.java @@ -21,11 +21,11 @@ /** * A cursor driver that uses the given query directly. - * + * * @hide */ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { - private String mEditTable; + private String mEditTable; private SQLiteDatabase mDatabase; private Cursor mCursor; private String mSql; @@ -69,7 +69,7 @@ public Cursor query(CursorFactory factory, String[] selectionArgs) { // Create the cursor if (factory == null) { mCursor = new SQLiteCursor(mDatabase, this, mEditTable, query); - + } else { mCursor = factory.newCursor(mDatabase, this, mEditTable, query); } diff --git a/src/net/sqlcipher/database/SQLiteOpenHelper.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteOpenHelper.java similarity index 76% rename from src/net/sqlcipher/database/SQLiteOpenHelper.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteOpenHelper.java index f7af8b91..e3a24f43 100644 --- a/src/net/sqlcipher/database/SQLiteOpenHelper.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteOpenHelper.java @@ -19,6 +19,7 @@ import java.io.File; import android.content.Context; +import android.database.sqlite.SQLiteException; import net.sqlcipher.DatabaseErrorHandler; import net.sqlcipher.DefaultDatabaseErrorHandler; import net.sqlcipher.database.SQLiteDatabaseHook; @@ -43,6 +44,8 @@ public abstract class SQLiteOpenHelper { private final int mNewVersion; private final SQLiteDatabaseHook mHook; private final DatabaseErrorHandler mErrorHandler; + private boolean mEnableWriteAheadLogging; + private boolean mDeferSetWriteAheadLoggingEnabled; private SQLiteDatabase mDatabase = null; private boolean mIsInitializing = false; @@ -79,7 +82,7 @@ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, SQLiteDatabaseHook hook) { this(context, name, factory, version, hook, new DefaultDatabaseErrorHandler()); } - + /** * Create a helper object to create, open, and/or manage a database. * The database is not actually created or opened until one of @@ -128,8 +131,12 @@ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, public synchronized SQLiteDatabase getWritableDatabase(String password) { return getWritableDatabase(password == null ? null : password.toCharArray()); } - + public synchronized SQLiteDatabase getWritableDatabase(char[] password) { + return getWritableDatabase(password == null ? null : SQLiteDatabase.getBytes(password)); + } + + public synchronized SQLiteDatabase getWritableDatabase(byte[] password) { if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) { return mDatabase; // The database is already open for business } @@ -150,19 +157,19 @@ public synchronized SQLiteDatabase getWritableDatabase(char[] password) { try { mIsInitializing = true; if (mName == null) { - db = SQLiteDatabase.create(null, password); - + db = SQLiteDatabase.create(null, ""); } else { String path = mContext.getDatabasePath(mName).getPath(); - File dbPathFile = new File (path); - if (!dbPathFile.exists()) + if (!dbPathFile.exists()) { dbPathFile.getParentFile().mkdirs(); - + } db = SQLiteDatabase.openOrCreateDatabase(path, password, mFactory, mHook, mErrorHandler); } - - + if(mDeferSetWriteAheadLoggingEnabled) { + mEnableWriteAheadLogging = db.enableWriteAheadLogging(); + } + onConfigure(db); int version = db.getVersion(); if (version != mNewVersion) { db.beginTransaction(); @@ -170,7 +177,11 @@ public synchronized SQLiteDatabase getWritableDatabase(char[] password) { if (version == 0) { onCreate(db); } else { + if(version > mNewVersion) { + onDowngrade(db, version, mNewVersion); + } else { onUpgrade(db, version, mNewVersion); + } } db.setVersion(mNewVersion); db.setTransactionSuccessful(); @@ -213,8 +224,12 @@ public synchronized SQLiteDatabase getWritableDatabase(char[] password) { public synchronized SQLiteDatabase getReadableDatabase(String password) { return getReadableDatabase(password == null ? null : password.toCharArray()); } - + public synchronized SQLiteDatabase getReadableDatabase(char[] password) { + return getReadableDatabase(password == null ? null : SQLiteDatabase.getBytes(password)); + } + + public synchronized SQLiteDatabase getReadableDatabase(byte[] password) { if (mDatabase != null && mDatabase.isOpen()) { return mDatabase; // The database is already open for business } @@ -236,7 +251,7 @@ public synchronized SQLiteDatabase getReadableDatabase(char[] password) { String path = mContext.getDatabasePath(mName).getPath(); File databasePath = new File(path); File databasesDirectory = new File(mContext.getDatabasePath(mName).getParent()); - + if(!databasesDirectory.exists()){ databasesDirectory.mkdirs(); } @@ -246,7 +261,7 @@ public synchronized SQLiteDatabase getReadableDatabase(char[] password) { mIsInitializing = true; db.close(); } - db = SQLiteDatabase.openDatabase(path, password, mFactory, SQLiteDatabase.OPEN_READONLY); + db = SQLiteDatabase.openDatabase(path, password, mFactory, SQLiteDatabase.OPEN_READONLY, mHook, mErrorHandler); if (db.getVersion() != mNewVersion) { throw new SQLiteException("Can't upgrade read-only database from version " + db.getVersion() + " to " + mNewVersion + ": " + path); @@ -274,6 +289,81 @@ public synchronized void close() { } } + /** + * Return the name of the SQLite database being opened, as given to + * the constructor. + */ + public String getDatabaseName() { + return mName; + } + + /** + * Enables or disables the use of write-ahead logging for the database. + * + * Write-ahead logging cannot be used with read-only databases so the value of + * this flag is ignored if the database is opened read-only. + * + * @param enabled True if write-ahead logging should be enabled, false if it + * should be disabled. + * + * @see SQLiteDatabase#enableWriteAheadLogging() + */ + public void setWriteAheadLoggingEnabled(boolean enabled) { + synchronized (this) { + if (mEnableWriteAheadLogging != enabled) { + if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) { + if (enabled) { + mDatabase.enableWriteAheadLogging(); + } else { + mDatabase.disableWriteAheadLogging(); + } + mEnableWriteAheadLogging = enabled; + } else { + mDeferSetWriteAheadLoggingEnabled = enabled; + } + } + } + } + + /** + * Called when the database needs to be downgraded. This is strictly similar to + * {@link #onUpgrade} method, but is called whenever current version is newer than requested one. + * However, this method is not abstract, so it is not mandatory for a customer to + * implement it. If not overridden, default implementation will reject downgrade and + * throws SQLiteException + * + *

+ * This method executes within a transaction. If an exception is thrown, all changes + * will automatically be rolled back. + *

+ * + * @param db The database. + * @param oldVersion The old database version. + * @param newVersion The new database version. + */ + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + throw new SQLiteException("Can't downgrade database from version " + + oldVersion + " to " + newVersion); + } + + /** + * Called when the database connection is being configured, to enable features + * such as write-ahead logging or foreign key support. + *

+ * This method is called before {@link #onCreate}, {@link #onUpgrade}, + * {@link #onDowngrade}, or {@link #onOpen} are called. It should not modify + * the database except to configure the database connection as required. + *

+ * This method should only call methods that configure the parameters of the + * database connection, such as {@link SQLiteDatabase#enableWriteAheadLogging} + * {@link SQLiteDatabase#setForeignKeyConstraintsEnabled}, + * {@link SQLiteDatabase#setLocale}, or executing PRAGMA statements. + *

+ * + * @param db The database. + */ + public void onConfigure(SQLiteDatabase db) {} + /** * Called when the database is created for the first time. This is where the * creation of tables and the initial population of the tables should happen. diff --git a/src/net/sqlcipher/database/SQLiteProgram.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteProgram.java similarity index 98% rename from src/net/sqlcipher/database/SQLiteProgram.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteProgram.java index b978bdf0..e43826d9 100644 --- a/src/net/sqlcipher/database/SQLiteProgram.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteProgram.java @@ -17,6 +17,7 @@ package net.sqlcipher.database; import android.util.Log; +import androidx.sqlite.db.SupportSQLiteProgram; /** * A base class for compiled SQLite programs. @@ -24,7 +25,8 @@ * SQLiteProgram is not internally synchronized so code using a SQLiteProgram from multiple * threads should perform its own synchronization when using the SQLiteProgram. */ -public abstract class SQLiteProgram extends SQLiteClosable { +public abstract class SQLiteProgram extends SQLiteClosable implements + SupportSQLiteProgram { private static final String TAG = "SQLiteProgram"; @@ -181,6 +183,7 @@ protected void compile(String sql, boolean forceCompilation) { * * @param index The 1-based index to the parameter to bind null to */ + @Override public void bindNull(int index) { if (mClosed) { throw new IllegalStateException("program already closed"); @@ -203,6 +206,7 @@ public void bindNull(int index) { * @param index The 1-based index to the parameter to bind * @param value The value to bind */ + @Override public void bindLong(int index, long value) { if (mClosed) { throw new IllegalStateException("program already closed"); @@ -225,6 +229,7 @@ public void bindLong(int index, long value) { * @param index The 1-based index to the parameter to bind * @param value The value to bind */ + @Override public void bindDouble(int index, double value) { if (mClosed) { throw new IllegalStateException("program already closed"); @@ -247,6 +252,7 @@ public void bindDouble(int index, double value) { * @param index The 1-based index to the parameter to bind * @param value The value to bind */ + @Override public void bindString(int index, String value) { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); @@ -272,6 +278,7 @@ public void bindString(int index, String value) { * @param index The 1-based index to the parameter to bind * @param value The value to bind */ + @Override public void bindBlob(int index, byte[] value) { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); @@ -293,6 +300,7 @@ public void bindBlob(int index, byte[] value) { /** * Clears all existing bindings. Unset bindings are treated as NULL. */ + @Override public void clearBindings() { if (mClosed) { throw new IllegalStateException("program already closed"); diff --git a/src/net/sqlcipher/database/SQLiteQuery.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteQuery.java similarity index 90% rename from src/net/sqlcipher/database/SQLiteQuery.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteQuery.java index d9c41a61..c87bd590 100644 --- a/src/net/sqlcipher/database/SQLiteQuery.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteQuery.java @@ -17,6 +17,8 @@ package net.sqlcipher.database; import net.sqlcipher.*; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.database.sqlite.SQLiteMisuseException; import android.os.SystemClock; import android.util.Log; @@ -65,8 +67,9 @@ public class SQLiteQuery extends SQLiteProgram { * @param window The window to fill into * @return number of total rows in the query */ - /* package */ int fillWindow(CursorWindow window, - int maxRead, int lastPos) { + /* package */ + int fillWindow(CursorWindow window, + int maxRead, int lastPos) { long timeStart = SystemClock.uptimeMillis(); mDatabase.lock(); try { @@ -76,8 +79,11 @@ public class SQLiteQuery extends SQLiteProgram { // if the start pos is not equal to 0, then most likely window is // too small for the data set, loading by another thread // is not safe in this situation. the native code will ignore maxRead - int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex, - maxRead, lastPos); + int numRows = native_fill_window(window, + window.getStartPosition(), + window.getRequiredPosition(), + mOffsetIndex, + maxRead, lastPos); // Logging if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { @@ -215,8 +221,10 @@ public void bindArguments(Object[] args){ } } - private final native int native_fill_window(CursorWindow window, - int startPos, int offsetParam, int maxRead, int lastPos); + private final native int native_fill_window(CursorWindow window, + int startPos, int requiredPos, + int offsetParam, int maxRead, + int lastPos); private final native int native_column_count(); diff --git a/src/net/sqlcipher/database/SQLiteQueryBuilder.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteQueryBuilder.java similarity index 100% rename from src/net/sqlcipher/database/SQLiteQueryBuilder.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteQueryBuilder.java diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteQueryStats.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteQueryStats.java new file mode 100644 index 00000000..4b36c05f --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteQueryStats.java @@ -0,0 +1,20 @@ +package net.sqlcipher.database; + +public class SQLiteQueryStats { + long totalQueryResultSize = 0L; + long largestIndividualRowSize = 0L; + + public SQLiteQueryStats(long totalQueryResultSize, + long largestIndividualRowSize) { + this.totalQueryResultSize = totalQueryResultSize; + this.largestIndividualRowSize = largestIndividualRowSize; + } + + public long getTotalQueryResultSize(){ + return totalQueryResultSize; + } + + public long getLargestIndividualRowSize(){ + return largestIndividualRowSize; + } +} diff --git a/src/net/sqlcipher/database/SQLiteStatement.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteStatement.java similarity index 96% rename from src/net/sqlcipher/database/SQLiteStatement.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteStatement.java index 0050b8b7..84b7b4c2 100644 --- a/src/net/sqlcipher/database/SQLiteStatement.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteStatement.java @@ -17,6 +17,7 @@ package net.sqlcipher.database; import android.os.SystemClock; +import androidx.sqlite.db.SupportSQLiteStatement; /** * A pre-compiled statement against a {@link SQLiteDatabase} that can be reused. @@ -27,7 +28,8 @@ * SQLiteStatement is not internally synchronized so code using a SQLiteStatement from multiple * threads should perform its own synchronization when using the SQLiteStatement. */ -public class SQLiteStatement extends SQLiteProgram +public class SQLiteStatement extends SQLiteProgram implements + SupportSQLiteStatement { /** * Don't use SQLiteStatement constructor directly, please use @@ -46,6 +48,7 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.SQLException If the SQL string is invalid for * some reason */ + @Override public void execute() { if (!mDatabase.isOpen()) { throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); @@ -71,6 +74,7 @@ public void execute() { * @throws android.database.SQLException If the SQL string is invalid for * some reason */ + @Override public long executeInsert() { if (!mDatabase.isOpen()) { throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); @@ -88,6 +92,7 @@ public long executeInsert() { } } + @Override public int executeUpdateDelete() { if (!mDatabase.isOpen()) { throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); @@ -113,6 +118,7 @@ public int executeUpdateDelete() { * * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ + @Override public long simpleQueryForLong() { if (!mDatabase.isOpen()) { throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); @@ -138,6 +144,7 @@ public long simpleQueryForLong() { * * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ + @Override public String simpleQueryForString() { if (!mDatabase.isOpen()) { throw new IllegalStateException("database " + mDatabase.getPath() + " already closed"); diff --git a/src/net/sqlcipher/database/SQLiteTransactionListener.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteTransactionListener.java similarity index 100% rename from src/net/sqlcipher/database/SQLiteTransactionListener.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SQLiteTransactionListener.java diff --git a/src/net/sqlcipher/database/SqliteWrapper.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SqliteWrapper.java similarity index 98% rename from src/net/sqlcipher/database/SqliteWrapper.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/SqliteWrapper.java index b1bbbfb4..1d15f99e 100644 --- a/src/net/sqlcipher/database/SqliteWrapper.java +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SqliteWrapper.java @@ -23,6 +23,7 @@ import net.sqlcipher.*; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.util.Log; import android.widget.Toast; diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SupportFactory.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SupportFactory.java new file mode 100644 index 00000000..2be2c2b2 --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SupportFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2019 Mark L. Murphy + * + * 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.sqlcipher.database; + +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class SupportFactory implements SupportSQLiteOpenHelper.Factory { + private final byte[] passphrase; + private final SQLiteDatabaseHook hook; + private final boolean clearPassphrase; + + public SupportFactory(byte[] passphrase) { + this(passphrase, (SQLiteDatabaseHook)null); + } + + public SupportFactory(byte[] passphrase, SQLiteDatabaseHook hook) { + this(passphrase, hook, true); + } + + public SupportFactory(byte[] passphrase, SQLiteDatabaseHook hook, + boolean clearPassphrase) { + this.passphrase = passphrase; + this.hook = hook; + this.clearPassphrase = clearPassphrase; + } + + @Override + public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) { + return new SupportHelper(configuration, passphrase, hook, clearPassphrase); + } +} diff --git a/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SupportHelper.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SupportHelper.java new file mode 100644 index 00000000..26960617 --- /dev/null +++ b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/SupportHelper.java @@ -0,0 +1,118 @@ + /* + * Copyright (C) 2019 Mark L. Murphy + * + * 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.sqlcipher.database; + +import android.database.sqlite.SQLiteException; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +public class SupportHelper implements SupportSQLiteOpenHelper { + private SQLiteOpenHelper standardHelper; + private byte[] passphrase; + private final boolean clearPassphrase; + + SupportHelper(final SupportSQLiteOpenHelper.Configuration configuration, + byte[] passphrase, final SQLiteDatabaseHook hook, + boolean clearPassphrase) { + SQLiteDatabase.loadLibs(configuration.context); + this.passphrase = passphrase; + this.clearPassphrase = clearPassphrase; + + standardHelper = + new SQLiteOpenHelper(configuration.context, configuration.name, + null, configuration.callback.version, hook) { + @Override + public void onCreate(SQLiteDatabase db) { + configuration.callback.onCreate(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, + int newVersion) { + configuration.callback.onUpgrade(db, oldVersion, + newVersion); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, + int newVersion) { + configuration.callback.onDowngrade(db, oldVersion, + newVersion); + } + + @Override + public void onOpen(SQLiteDatabase db) { + configuration.callback.onOpen(db); + } + + @Override + public void onConfigure(SQLiteDatabase db) { + configuration.callback.onConfigure(db); + } + }; + } + + @Override + public String getDatabaseName() { + return standardHelper.getDatabaseName(); + } + + @Override + public void setWriteAheadLoggingEnabled(boolean enabled) { + standardHelper.setWriteAheadLoggingEnabled(enabled); + } + + @Override + public SupportSQLiteDatabase getWritableDatabase() { + SQLiteDatabase result; + try { + result = standardHelper.getWritableDatabase(passphrase); + } catch (SQLiteException ex){ + if(passphrase != null){ + boolean isCleared = true; + for(byte b : passphrase){ + isCleared = isCleared && (b == (byte)0); + } + if (isCleared) { + throw new IllegalStateException("The passphrase appears to be cleared. This happens by " + + "default the first time you use the factory to open a database, so we can remove the " + + "cleartext passphrase from memory. If you close the database yourself, please use a " + + "fresh SupportFactory to reopen it. If something else (e.g., Room) closed the " + + "database, and you cannot control that, use SupportFactory boolean constructor option " + + "to opt out of the automatic password clearing step. See the project README for more information.", ex); + } + } + throw ex; + } + if(clearPassphrase && passphrase != null) { + for (int i = 0; i < passphrase.length; i++) { + passphrase[i] = (byte)0; + } + } + return result; + } + + @Override + public SupportSQLiteDatabase getReadableDatabase() { + return getWritableDatabase(); + } + + @Override + public void close() { + standardHelper.close(); + } +} diff --git a/src/net/sqlcipher/database/package-info.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/database/package-info.java similarity index 100% rename from src/net/sqlcipher/database/package-info.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/database/package-info.java diff --git a/src/net/sqlcipher/package-info.java b/android-database-sqlcipher/src/main/java/net/sqlcipher/package-info.java similarity index 100% rename from src/net/sqlcipher/package-info.java rename to android-database-sqlcipher/src/main/java/net/sqlcipher/package-info.java diff --git a/android-database-sqlcipher/src/main/res/values/android_database_sqlcipher_strings.xml b/android-database-sqlcipher/src/main/res/values/android_database_sqlcipher_strings.xml new file mode 100644 index 00000000..ddedb587 --- /dev/null +++ b/android-database-sqlcipher/src/main/res/values/android_database_sqlcipher_strings.xml @@ -0,0 +1,12 @@ + + + Zetetic, LLC + https://www.zetetic.net/sqlcipher/ + SQLCipher for Android + Android SQLite API based on SQLCipher + https://www.zetetic.net/sqlcipher/ + ${clientVersionNumber} + true + https://github.com/sqlcipher/android-database-sqlcipher + https://www.zetetic.net/sqlcipher/license/ + diff --git a/build-openssl-libraries.sh b/build-openssl-libraries.sh deleted file mode 100755 index 917fe72b..00000000 --- a/build-openssl-libraries.sh +++ /dev/null @@ -1,116 +0,0 @@ -#! /usr/bin/env bash -(cd external/openssl; - - if [ ! ${ANDROID_NDK_ROOT} ]; then - echo "ANDROID_NDK_ROOT environment variable not set, set and rerun" - exit 1 - fi - - ANDROID_LIB_ROOT=../android-libs - ANDROID_TOOLCHAIN_DIR=/tmp/sqlcipher-android-toolchain - OPENSSL_CONFIGURE_OPTIONS="no-krb5 no-idea no-camellia \ - no-seed no-bf no-cast no-rc2 no-rc4 no-rc5 no-md2 \ - no-md4 no-ripemd no-rsa no-ecdh no-sock no-ssl2 no-ssl3 \ - no-dsa no-dh no-ec no-ecdsa no-tls1 no-pbe no-pkcs \ - no-tlsext no-pem no-rfc3779 no-whirlpool no-ui no-srp \ - no-ssltrace no-tlsext no-mdc2 no-ecdh no-engine \ - no-tls2 no-srtp -fPIC" - - HOST_INFO=`uname -a` - case ${HOST_INFO} in - Darwin*) - TOOLCHAIN_SYSTEM=darwin-x86 - ;; - Linux*) - if [[ "${HOST_INFO}" == *i686* ]] - then - TOOLCHAIN_SYSTEM=linux-x86 - else - TOOLCHAIN_SYSTEM=linux-x86_64 - fi - ;; - *) - echo "Toolchain unknown for host system" - exit 1 - ;; - esac - - rm -rf ${ANDROID_LIB_ROOT} - git clean -dfx && git checkout -f - ./Configure dist - - for SQLCIPHER_TARGET_PLATFORM in armeabi armeabi-v7a x86 x86_64 arm64-v8a - do - echo "Building for libcrypto.a for ${SQLCIPHER_TARGET_PLATFORM}" - case "${SQLCIPHER_TARGET_PLATFORM}" in - armeabi) - TOOLCHAIN_ARCH=arm - TOOLCHAIN_PREFIX=arm-linux-androideabi - CONFIGURE_ARCH=android - PLATFORM_OUTPUT_DIR=armeabi - ANDROID_PLATFORM_VERSION=android-9 - ;; - armeabi-v7a) - TOOLCHAIN_ARCH=arm - TOOLCHAIN_PREFIX=arm-linux-androideabi - CONFIGURE_ARCH=android -march=armv7-a - PLATFORM_OUTPUT_DIR=armeabi-v7a - ANDROID_PLATFORM_VERSION=android-9 - ;; - x86) - TOOLCHAIN_ARCH=x86 - TOOLCHAIN_PREFIX=i686-linux-android - CONFIGURE_ARCH=android-x86 - PLATFORM_OUTPUT_DIR=x86 - ANDROID_PLATFORM_VERSION=android-9 - ;; - x86_64) - TOOLCHAIN_ARCH=x86_64 - TOOLCHAIN_PREFIX=x86_64-linux-android - CONFIGURE_ARCH=android64 - PLATFORM_OUTPUT_DIR=x86_64 - ANDROID_PLATFORM_VERSION=android-21 - ;; - arm64-v8a) - TOOLCHAIN_ARCH=arm64 - TOOLCHAIN_PREFIX=aarch64-linux-android - CONFIGURE_ARCH=android64-aarch64 - PLATFORM_OUTPUT_DIR=arm64-v8a - ANDROID_PLATFORM_VERSION=android-21 - ;; - *) - echo "Unsupported build platform:${SQLCIPHER_TARGET_PLATFORM}" - exit 1 - esac - - rm -rf ${ANDROID_TOOLCHAIN_DIR} - mkdir -p "${ANDROID_LIB_ROOT}/${SQLCIPHER_TARGET_PLATFORM}" - ${ANDROID_NDK_ROOT}/build/tools/make-standalone-toolchain.sh \ - --platform=${ANDROID_PLATFORM_VERSION} \ - --install-dir=${ANDROID_TOOLCHAIN_DIR} \ - --arch=${TOOLCHAIN_ARCH} - - export PATH=${ANDROID_TOOLCHAIN_DIR}/bin:$PATH - export CROSS_SYSROOT=${ANDROID_TOOLCHAIN_DIR}/sysroot - - RANLIB=${TOOLCHAIN_PREFIX}-ranlib \ - AR=${TOOLCHAIN_PREFIX}-ar \ - CC=${TOOLCHAIN_PREFIX}-gcc \ - ./Configure "${CONFIGURE_ARCH}" "${OPENSSL_CONFIGURE_OPTIONS}" - - if [ $? -ne 0 ]; then - echo "Error executing:./Configure ${CONFIGURE_ARCH} ${OPENSSL_CONFIGURE_OPTIONS}" - exit 1 - fi - - make clean - make - - if [ $? -ne 0 ]; then - echo "Error executing make for platform:${SQLCIPHER_TARGET_PLATFORM}" - exit 1 - fi - - mv libcrypto.a ${ANDROID_LIB_ROOT}/${PLATFORM_OUTPUT_DIR} - done -) diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..0751426a --- /dev/null +++ b/build.gradle @@ -0,0 +1,99 @@ +buildscript { + repositories { + google() + mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath 'com.android.tools.build:gradle:7.3.1' + classpath "gradle.plugin.org.ec4j.gradle:editorconfig-gradle-plugin:0.0.3" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +ext { + if(project.hasProperty('sqlcipherAndroidClientVersion')) { + clientVersionNumber = "${sqlcipherAndroidClientVersion}" + } else { + clientVersionNumber = "UndefinedBuildNumber" + } + mavenPackaging = "aar" + mavenGroup = "net.zetetic" + mavenArtifactId = "android-database-sqlcipher" + mavenLocalRepositoryPrefix = "file://" + if(project.hasProperty('publishLocal') && publishLocal.toBoolean()){ + mavenSnapshotRepositoryUrl = "outputs/snapshot" + mavenReleaseRepositoryUrl = "outputs/release" + } else { + mavenLocalRepositoryPrefix = "" + mavenSnapshotRepositoryUrl = "https://oss.sonatype.org/content/repositories/snapshots" + mavenReleaseRepositoryUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2" + } + if(project.hasProperty('publishSnapshot') && publishSnapshot.toBoolean()){ + mavenVersionName = "${clientVersionNumber}-SNAPSHOT" + } else { + mavenVersionName = "${clientVersionNumber}" + } + if(project.hasProperty('nexusUsername')){ + nexusUsername = "${nexusUsername}" + } + if(project.hasProperty('nexusPassword')){ + nexusPassword = "${nexusPassword}" + } + mavenPomDescription = "SQLCipher for Android is a plugin to SQLite that provides full database encryption." + mavenPomUrl = "https://www.zetetic.net/sqlcipher" + mavenScmUrl = "https://github.com/sqlcipher/android-database-sqlcipher.git" + mavenScmConnection = "scm:git:https://github.com/sqlcipher/android-database-sqlcipher.git" + mavenScmDeveloperConnection = "scm:git:https://github.com/sqlcipher/android-database-sqlcipher.git" + mavenLicenseUrl = "https://www.zetetic.net/sqlcipher/license/" + mavenDeveloperName = "Zetetic Support" + mavenDeveloperEmail = "support@zetetic.net" + mavenDeveloperOrganization = "Zetetic LLC" + mavenDeveloperUrl = "https://www.zetetic.net" + minimumAndroidSdkVersion = 21 + minimumAndroid64BitSdkVersion = 21 + targetAndroidSdkVersion = 26 + compileAndroidSdkVersion = 26 + mainProjectName = "android-database-sqlcipher" + nativeRootOutputDir = "${projectDir}/${mainProjectName}/src/main" + if(project.hasProperty('sqlcipherRoot')) { + sqlcipherDir = "${sqlcipherRoot}" + } + if(project.hasProperty('opensslAndroidNativeRoot') && "${opensslAndroidNativeRoot}") { + androidNativeRootDir = "${opensslAndroidNativeRoot}" + } else { + androidNativeRootDir = "${nativeRootOutputDir}/external/android-libs" + } + if(project.hasProperty('opensslRoot')) { + opensslDir = "${opensslRoot}" + } + if(project.hasProperty('debugBuild') && debugBuild.toBoolean()) { + otherSqlcipherCFlags = "-fstack-protector-all" + ndkBuildType="NDK_DEBUG=1" + } else { + otherSqlcipherCFlags = "-DLOG_NDEBUG -fstack-protector-all" + ndkBuildType="NDK_DEBUG=0" + } + if(project.hasProperty('sqlcipherCFlags') + && project.sqlcipherCFlags?.trim() + && project.sqlcipherCFlags?.contains('SQLITE_HAS_CODEC') + && project.sqlcipherCFlags?.contains('SQLITE_TEMP_STORE')) { + sqlcipherCFlags = "${sqlcipherCFlags}" + } else { + if(!project.gradle.startParameter.taskNames.toString().contains('clean')){ + throw new InvalidUserDataException("SQLCIPHER_CFLAGS environment variable must be specified and include at least '-DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=2'") + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/custom_rules.xml b/custom_rules.xml deleted file mode 100644 index 5402a311..00000000 --- a/custom_rules.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/external/openssl b/external/openssl deleted file mode 160000 index 91eaf079..00000000 --- a/external/openssl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 91eaf079b7430cb4ebb7f3ccabe74aa383b27c4e diff --git a/external/sqlcipher b/external/sqlcipher deleted file mode 160000 index df092f0a..00000000 --- a/external/sqlcipher +++ /dev/null @@ -1 +0,0 @@ -Subproject commit df092f0a7af1c8e3558a743036c089e6ef8e6307 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..2d8d1e4d --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..41d9927a Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..070cb702 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jars/doclava-1.0.6.jar b/jars/doclava-1.0.6.jar deleted file mode 100644 index 6f66dac6..00000000 Binary files a/jars/doclava-1.0.6.jar and /dev/null differ diff --git a/jni/Application32.mk b/jni/Application32.mk deleted file mode 100644 index 8cbd6fc4..00000000 --- a/jni/Application32.mk +++ /dev/null @@ -1,5 +0,0 @@ -APP_PROJECT_PATH := $(shell pwd) -APP_ABI := armeabi armeabi-v7a x86 -APP_PLATFORM := android-9 -APP_BUILD_SCRIPT := $(APP_PROJECT_PATH)/Android.mk -APP_STL := stlport_static diff --git a/jni/Application64.mk b/jni/Application64.mk deleted file mode 100644 index 794b44ad..00000000 --- a/jni/Application64.mk +++ /dev/null @@ -1,5 +0,0 @@ -APP_PROJECT_PATH := $(shell pwd) -APP_ABI := x86_64 arm64-v8a -APP_PLATFORM := android-21 -APP_BUILD_SCRIPT := $(APP_PROJECT_PATH)/Android.mk -APP_STL := stlport_static diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 075f0c2a..00000000 --- a/pom.xml +++ /dev/null @@ -1,134 +0,0 @@ - - 4.0.0 - net.zetetic - android-database-sqlcipher - 3.5.7 - aar - android-database-sqlcipher - - SQLCipher for Android is a plugin to SQLite that provides full database encryption. - - https://www.zetetic.net/sqlcipher/ - - - https://www.zetetic.net/sqlcipher/license/ - - - - - Zetetic Support - support@zetetic.net - Zetetic LLC - https://www.zetetic.net/ - - - - scm:git:https://github.com/sqlcipher/android-database-sqlcipher.git - scm:git:https://github.com/sqlcipher/android-database-sqlcipher.git - https://github.com/sqlcipher/android-database-sqlcipher.git - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - bin/ - - - com.simpligility.maven.plugins - android-maven-plugin - 4.3.0 - true - - - 23 - - - false - - AndroidManifest.xml - res - assets - libs - true - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - 97ED25C2 - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 - true - - ossrh - https://oss.sonatype.org/ - false - - - - com.thoughtworks.xstream - xstream - 1.4.7 - - - - - - - - com.google.android - android - 4.0.1.2 - provided - - - diff --git a/project.properties b/project.properties deleted file mode 100644 index 19c32e63..00000000 --- a/project.properties +++ /dev/null @@ -1,11 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system use, -# "ant.properties", and override values to adapt the script to your -# project structure. - -# Project target. -target=android-23 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..79439766 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':android-database-sqlcipher' diff --git a/src/net/sqlcipher/SQLException.java b/src/net/sqlcipher/SQLException.java deleted file mode 100644 index 8c8c0373..00000000 --- a/src/net/sqlcipher/SQLException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -package net.sqlcipher; - -/** - * An exception that indicates there was an error with SQL parsing or execution. - */ -public class SQLException extends RuntimeException -{ - public SQLException() {} - - public SQLException(String error) - { - super(error); - } -} diff --git a/src/net/sqlcipher/database/SQLiteAbortException.java b/src/net/sqlcipher/database/SQLiteAbortException.java deleted file mode 100644 index 89b066ce..00000000 --- a/src/net/sqlcipher/database/SQLiteAbortException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2008 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.sqlcipher.database; - -/** - * An exception that indicates that the SQLite program was aborted. - * This can happen either through a call to ABORT in a trigger, - * or as the result of using the ABORT conflict clause. - */ -public class SQLiteAbortException extends SQLiteException { - public SQLiteAbortException() {} - - public SQLiteAbortException(String error) { - super(error); - } -} diff --git a/src/net/sqlcipher/database/SQLiteConstraintException.java b/src/net/sqlcipher/database/SQLiteConstraintException.java deleted file mode 100644 index d9d548f6..00000000 --- a/src/net/sqlcipher/database/SQLiteConstraintException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2008 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.sqlcipher.database; - -/** - * An exception that indicates that an integrity constraint was violated. - */ -public class SQLiteConstraintException extends SQLiteException { - public SQLiteConstraintException() {} - - public SQLiteConstraintException(String error) { - super(error); - } -} diff --git a/src/net/sqlcipher/database/SQLiteDatabase.java b/src/net/sqlcipher/database/SQLiteDatabase.java deleted file mode 100644 index d9408f96..00000000 --- a/src/net/sqlcipher/database/SQLiteDatabase.java +++ /dev/null @@ -1,2918 +0,0 @@ -/* - * 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. - */ - -package net.sqlcipher.database; - -import net.sqlcipher.Cursor; -import net.sqlcipher.CrossProcessCursorWrapper; -import net.sqlcipher.DatabaseUtils; -import net.sqlcipher.DatabaseErrorHandler; -import net.sqlcipher.DefaultDatabaseErrorHandler; -import net.sqlcipher.SQLException; -import net.sqlcipher.database.SQLiteDebug.DbStats; -import net.sqlcipher.database.SQLiteDatabaseHook; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.Charset; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.locks.ReentrantLock; -import java.util.regex.Pattern; -import java.util.zip.ZipInputStream; - -import android.content.ContentValues; - -import android.content.Context; - -import android.os.Debug; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.Config; -import android.util.Log; -import android.util.Pair; - -import java.io.UnsupportedEncodingException; - -/** - * Exposes methods to manage a SQLCipher database. - *

SQLiteDatabase has methods to create, delete, execute SQL commands, and - * perform other common database management tasks. - *

A call to loadLibs(…) should occur before attempting to - * create or open a database connection. - *

Database names must be unique within an application, not across all - * applications. - * - */ -public class SQLiteDatabase extends SQLiteClosable { - private static final String TAG = "Database"; - private static final int EVENT_DB_OPERATION = 52000; - private static final int EVENT_DB_CORRUPT = 75004; - private static final String KEY_ENCODING = "UTF-8"; - - /** - * The version number of the SQLCipher for Android Java client library. - */ - public static final String SQLCIPHER_ANDROID_VERSION = "3.5.7"; - - // Stores reference to all databases opened in the current process. - // (The referent Object is not used at this time.) - // INVARIANT: Guarded by sActiveDatabases. - private static WeakHashMap sActiveDatabases = - new WeakHashMap(); - - public int status(int operation, boolean reset){ - return native_status(operation, reset); - } - - /** - * Change the password of the open database using sqlite3_rekey(). - * - * @param password new database password - * - * @throws SQLiteException if there is an issue changing the password internally - * OR if the database is not open - * - * FUTURE @todo throw IllegalStateException if the database is not open and - * update the test suite - */ - public void changePassword(String password) throws SQLiteException { - /* safeguard: */ - if (!isOpen()) { - throw new SQLiteException("database not open"); - } - if (password != null) { - byte[] keyMaterial = getBytes(password.toCharArray()); - rekey(keyMaterial); - for(byte data : keyMaterial) { - data = 0; - } - } - } - - /** - * Change the password of the open database using sqlite3_rekey(). - * - * @param password new database password (char array) - * - * @throws SQLiteException if there is an issue changing the password internally - * OR if the database is not open - * - * FUTURE @todo throw IllegalStateException if the database is not open and - * update the test suite - */ - public void changePassword(char[] password) throws SQLiteException { - /* safeguard: */ - if (!isOpen()) { - throw new SQLiteException("database not open"); - } - if (password != null) { - byte[] keyMaterial = getBytes(password); - rekey(keyMaterial); - for(byte data : keyMaterial) { - data = 0; - } - } - } - - private static void loadICUData(Context context, File workingDir) { - OutputStream out = null; - ZipInputStream in = null; - File icuDir = new File(workingDir, "icu"); - File icuDataFile = new File(icuDir, "icudt46l.dat"); - try { - if(!icuDir.exists()) icuDir.mkdirs(); - if(!icuDataFile.exists()) { - in = new ZipInputStream(context.getAssets().open("icudt46l.zip")); - in.getNextEntry(); - out = new FileOutputStream(icuDataFile); - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } - } - catch (Exception ex) { - Log.e(TAG, "Error copying icu dat file", ex); - if(icuDataFile.exists()){ - icuDataFile.delete(); - } - throw new RuntimeException(ex); - } - finally { - try { - if(in != null){ - in.close(); - } - if(out != null){ - out.flush(); - out.close(); - } - } catch (IOException ioe){ - Log.e(TAG, "Error in closing streams IO streams after expanding ICU dat file", ioe); - throw new RuntimeException(ioe); - } - } - } - - /** - * Implement this interface to provide custom strategy for loading jni libraries. - */ - public interface LibraryLoader { - /** - * Load jni libraries by given names. - * Straightforward implementation will be calling {@link System#loadLibrary(String name)} - * for every provided library name. - * - * @param libNames library names that sqlcipher need to load - */ - void loadLibraries(String... libNames); - } - - /** - * Loads the native SQLCipher library into the application process. - */ - public static synchronized void loadLibs (Context context) { - loadLibs(context, context.getFilesDir()); - } - - /** - * Loads the native SQLCipher library into the application process. - */ - public static synchronized void loadLibs (Context context, File workingDir) { - loadLibs(context, workingDir, new LibraryLoader() { - @Override - public void loadLibraries(String... libNames) { - for (String libName : libNames) { - System.loadLibrary(libName); - } - } - }); - } - - /** - * Loads the native SQLCipher library into the application process. - */ - public static synchronized void loadLibs(Context context, LibraryLoader libraryLoader) { - loadLibs(context, context.getFilesDir(), libraryLoader); - } - - /** - * Loads the native SQLCipher library into the application process. - */ - public static synchronized void loadLibs (Context context, File workingDir, LibraryLoader libraryLoader) { - libraryLoader.loadLibraries("sqlcipher"); - - // System.loadLibrary("stlport_shared"); - // System.loadLibrary("sqlcipher_android"); - // System.loadLibrary("database_sqlcipher"); - - // boolean systemICUFileExists = new File("/system/usr/icu/icudt46l.dat").exists(); - - // String icuRootPath = systemICUFileExists ? "/system/usr" : workingDir.getAbsolutePath(); - // setICURoot(icuRootPath); - // if(!systemICUFileExists){ - // loadICUData(context, workingDir); - // } - } - - /** - * Algorithms used in ON CONFLICT clause - * http://www.sqlite.org/lang_conflict.html - */ - /** - * When a constraint violation occurs, an immediate ROLLBACK occurs, - * thus ending the current transaction, and the command aborts with a - * return code of SQLITE_CONSTRAINT. If no transaction is active - * (other than the implied transaction that is created on every command) - * then this algorithm works the same as ABORT. - */ - public static final int CONFLICT_ROLLBACK = 1; - - /** - * When a constraint violation occurs,no ROLLBACK is executed - * so changes from prior commands within the same transaction - * are preserved. This is the default behavior. - */ - public static final int CONFLICT_ABORT = 2; - - /** - * When a constraint violation occurs, the command aborts with a return - * code SQLITE_CONSTRAINT. But any changes to the database that - * the command made prior to encountering the constraint violation - * are preserved and are not backed out. - */ - public static final int CONFLICT_FAIL = 3; - - /** - * When a constraint violation occurs, the one row that contains - * the constraint violation is not inserted or changed. - * But the command continues executing normally. Other rows before and - * after the row that contained the constraint violation continue to be - * inserted or updated normally. No error is returned. - */ - public static final int CONFLICT_IGNORE = 4; - - /** - * When a UNIQUE constraint violation occurs, the pre-existing rows that - * are causing the constraint violation are removed prior to inserting - * or updating the current row. Thus the insert or update always occurs. - * The command continues executing normally. No error is returned. - * If a NOT NULL constraint violation occurs, the NULL value is replaced - * by the default value for that column. If the column has no default - * value, then the ABORT algorithm is used. If a CHECK constraint - * violation occurs then the IGNORE algorithm is used. When this conflict - * resolution strategy deletes rows in order to satisfy a constraint, - * it does not invoke delete triggers on those rows. - * This behavior might change in a future release. - */ - public static final int CONFLICT_REPLACE = 5; - - /** - * use the following when no conflict action is specified. - */ - public static final int CONFLICT_NONE = 0; - private static final String[] CONFLICT_VALUES = new String[] - {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; - - /** - * Maximum Length Of A LIKE Or GLOB Pattern - * The pattern matching algorithm used in the default LIKE and GLOB implementation - * of SQLite can exhibit O(N^2) performance (where N is the number of characters in - * the pattern) for certain pathological cases. To avoid denial-of-service attacks - * the length of the LIKE or GLOB pattern is limited to SQLITE_MAX_LIKE_PATTERN_LENGTH bytes. - * The default value of this limit is 50000. A modern workstation can evaluate - * even a pathological LIKE or GLOB pattern of 50000 bytes relatively quickly. - * The denial of service problem only comes into play when the pattern length gets - * into millions of bytes. Nevertheless, since most useful LIKE or GLOB patterns - * are at most a few dozen bytes in length, paranoid application developers may - * want to reduce this parameter to something in the range of a few hundred - * if they know that external users are able to generate arbitrary patterns. - */ - public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000; - - /** - * Flag for {@link #openDatabase} to open the database for reading and writing. - * If the disk is full, this may fail even before you actually write anything. - * - * {@more} Note that the value of this flag is 0, so it is the default. - */ - public static final int OPEN_READWRITE = 0x00000000; // update native code if changing - - /** - * Flag for {@link #openDatabase} to open the database for reading only. - * This is the only reliable way to open a database if the disk may be full. - */ - public static final int OPEN_READONLY = 0x00000001; // update native code if changing - - private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing - - /** - * Flag for {@link #openDatabase} to open the database without support for localized collators. - * - * {@more} This causes the collator LOCALIZED not to be created. - * You must be consistent when using this flag to use the setting the database was - * created with. If this is set, {@link #setLocale} will do nothing. - */ - public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing - - /** - * Flag for {@link #openDatabase} to create the database file if it does not already exist. - */ - public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing - - /** - * SQLite memory database name - */ - public static final String MEMORY = ":memory:"; - - /** - * Indicates whether the most-recently started transaction has been marked as successful. - */ - private boolean mInnerTransactionIsSuccessful; - - /** - * Valid during the life of a transaction, and indicates whether the entire transaction (the - * outer one and all of the inner ones) so far has been successful. - */ - private boolean mTransactionIsSuccessful; - - /** - * Valid during the life of a transaction. - */ - private SQLiteTransactionListener mTransactionListener; - - /** Synchronize on this when accessing the database */ - private final ReentrantLock mLock = new ReentrantLock(true); - - private long mLockAcquiredWallTime = 0L; - private long mLockAcquiredThreadTime = 0L; - - // limit the frequency of complaints about each database to one within 20 sec - // unless run command adb shell setprop log.tag.Database VERBOSE - private static final int LOCK_WARNING_WINDOW_IN_MS = 20000; - /** If the lock is held this long then a warning will be printed when it is released. */ - private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300; - private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100; - private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000; - - private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000; - - // The pattern we remove from database filenames before - // potentially logging them. - private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); - - private long mLastLockMessageTime = 0L; - - // Things related to query logging/sampling for debugging - // slow/frequent queries during development. Always log queries - // which take (by default) 500ms+; shorter queries are sampled - // accordingly. Commit statements, which are typically slow, are - // logged together with the most recently executed SQL statement, - // for disambiguation. The 500ms value is configurable via a - // SystemProperty, but developers actively debugging database I/O - // should probably use the regular log tunable, - // LOG_SLOW_QUERIES_PROPERTY, defined below. - private static int sQueryLogTimeInMillis = 0; // lazily initialized - private static final int QUERY_LOG_SQL_LENGTH = 64; - private static final String COMMIT_SQL = "COMMIT;"; - private String mLastSqlStatement = null; - - // String prefix for slow database query EventLog records that show - // lock acquistions of the database. - /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; - - /** Used by native code, do not rename */ - /* package */ long mNativeHandle = 0; - - /** Used to make temp table names unique */ - /* package */ int mTempTableSequence = 0; - - /** The path for the database file */ - private String mPath; - - /** The anonymized path for the database file for logging purposes */ - private String mPathForLogs = null; // lazily populated - - /** The flags passed to open/create */ - private int mFlags; - - /** The optional factory to use when creating new Cursors */ - private CursorFactory mFactory; - - private WeakHashMap mPrograms; - - /** - * for each instance of this class, a cache is maintained to store - * the compiled query statement ids returned by sqlite database. - * key = sql statement with "?" for bind args - * value = {@link SQLiteCompiledSql} - * If an application opens the database and keeps it open during its entire life, then - * there will not be an overhead of compilation of sql statements by sqlite. - * - * why is this cache NOT static? because sqlite attaches compiledsql statements to the - * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is - * invoked. - * - * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method - * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because - * most of the apps don't use "?" syntax in their sql, caching is not useful for them. - */ - /* package */ Map mCompiledQueries = new HashMap(); - /** - * @hide - */ - public static final int MAX_SQL_CACHE_SIZE = 250; - private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance - private int mCacheFullWarnings; - private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1; - - /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors - * Corruption - * */ - private final DatabaseErrorHandler mErrorHandler; - - /** maintain stats about number of cache hits and misses */ - private int mNumCacheHits; - private int mNumCacheMisses; - - /** the following 2 members maintain the time when a database is opened and closed */ - private String mTimeOpened = null; - private String mTimeClosed = null; - - /** Used to find out where this object was created in case it never got closed. */ - private Throwable mStackTrace = null; - - // System property that enables logging of slow queries. Specify the threshold in ms. - private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; - private final int mSlowQueryThreshold; - - /** - * @param closable - */ - void addSQLiteClosable(SQLiteClosable closable) { - lock(); - try { - mPrograms.put(closable, null); - } finally { - unlock(); - } - } - - void removeSQLiteClosable(SQLiteClosable closable) { - lock(); - try { - mPrograms.remove(closable); - } finally { - unlock(); - } - } - - @Override - protected void onAllReferencesReleased() { - if (isOpen()) { - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeClosed = getTime(); - } - dbclose(); - - synchronized (sActiveDatabases) { - sActiveDatabases.remove(this); - } - } - } - - /** - * Attempts to release memory that SQLite holds but does not require to - * operate properly. Typically this memory will come from the page cache. - * - * @return the number of bytes actually released - */ - static public native int releaseMemory(); - - /** - * Control whether or not the SQLiteDatabase is made thread-safe by using locks - * around critical sections. This is pretty expensive, so if you know that your - * DB will only be used by a single thread then you should set this to false. - * The default is true. - * @param lockingEnabled set to true to enable locks, false otherwise - */ - public void setLockingEnabled(boolean lockingEnabled) { - mLockingEnabled = lockingEnabled; - } - - /** - * If set then the SQLiteDatabase is made thread-safe by using locks - * around critical sections - */ - private boolean mLockingEnabled = true; - - /* package */ void onCorruption() { - Log.e(TAG, "Calling error handler for corrupt database (detected) " + mPath); - - // NOTE: DefaultDatabaseErrorHandler deletes the corrupt file, EXCEPT for memory database - mErrorHandler.onCorruption(this); - } - - /** - * Locks the database for exclusive access. The database lock must be held when - * touch the native sqlite3* object since it is single threaded and uses - * a polling lock contention algorithm. The lock is recursive, and may be acquired - * multiple times by the same thread. This is a no-op if mLockingEnabled is false. - * - * @see #unlock() - */ - /* package */ void lock() { - if (!mLockingEnabled) return; - mLock.lock(); - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - // Use elapsed real-time since the CPU may sleep when waiting for IO - mLockAcquiredWallTime = SystemClock.elapsedRealtime(); - mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); - } - } - } - - /** - * Locks the database for exclusive access. The database lock must be held when - * touch the native sqlite3* object since it is single threaded and uses - * a polling lock contention algorithm. The lock is recursive, and may be acquired - * multiple times by the same thread. - * - * @see #unlockForced() - */ - private void lockForced() { - mLock.lock(); - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - // Use elapsed real-time since the CPU may sleep when waiting for IO - mLockAcquiredWallTime = SystemClock.elapsedRealtime(); - mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); - } - } - } - - /** - * Releases the database lock. This is a no-op if mLockingEnabled is false. - * - * @see #unlock() - */ - /* package */ void unlock() { - if (!mLockingEnabled) return; - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - checkLockHoldTime(); - } - } - mLock.unlock(); - } - - /** - * Releases the database lock. - * - * @see #unlockForced() - */ - private void unlockForced() { - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - checkLockHoldTime(); - } - } - mLock.unlock(); - } - - private void checkLockHoldTime() { - // Use elapsed real-time since the CPU may sleep when waiting for IO - long elapsedTime = SystemClock.elapsedRealtime(); - long lockedTime = elapsedTime - mLockAcquiredWallTime; - if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT && - !Log.isLoggable(TAG, Log.VERBOSE) && - (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) { - return; - } - if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) { - int threadTime = (int) - ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000); - if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS || - lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) { - mLastLockMessageTime = elapsedTime; - String msg = "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was " - + threadTime + "ms"; - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) { - Log.d(TAG, msg, new Exception()); - } else { - Log.d(TAG, msg); - } - } - } - } - - /** - * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of - * the work done in that transaction and all of the nested transactions will be committed or - * rolled back. The changes will be rolled back if any transaction is ended without being - * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. - * - *

Here is the standard idiom for transactions: - * - *

-     *   db.beginTransaction();
-     *   try {
-     *     ...
-     *     db.setTransactionSuccessful();
-     *   } finally {
-     *     db.endTransaction();
-     *   }
-     * 
- * - * @throws IllegalStateException if the database is not open - */ - public void beginTransaction() { - beginTransactionWithListener(null /* transactionStatusCallback */); - } - - /** - * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of - * the work done in that transaction and all of the nested transactions will be committed or - * rolled back. The changes will be rolled back if any transaction is ended without being - * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. - * - *

Here is the standard idiom for transactions: - * - *

-     *   db.beginTransactionWithListener(listener);
-     *   try {
-     *     ...
-     *     db.setTransactionSuccessful();
-     *   } finally {
-     *     db.endTransaction();
-     *   }
-     * 
- * @param transactionListener listener that should be notified when the transaction begins, - * commits, or is rolled back, either explicitly or by a call to - * {@link #yieldIfContendedSafely}. - * - * @throws IllegalStateException if the database is not open - */ - public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { - lockForced(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - boolean ok = false; - try { - // If this thread already had the lock then get out - if (mLock.getHoldCount() > 1) { - if (mInnerTransactionIsSuccessful) { - String msg = "Cannot call beginTransaction between " - + "calling setTransactionSuccessful and endTransaction"; - IllegalStateException e = new IllegalStateException(msg); - Log.e(TAG, "beginTransaction() failed", e); - throw e; - } - ok = true; - return; - } - - // This thread didn't already have the lock, so begin a database - // transaction now. - execSQL("BEGIN EXCLUSIVE;"); - mTransactionListener = transactionListener; - mTransactionIsSuccessful = true; - mInnerTransactionIsSuccessful = false; - if (transactionListener != null) { - try { - transactionListener.onBegin(); - } catch (RuntimeException e) { - execSQL("ROLLBACK;"); - throw e; - } - } - ok = true; - } finally { - if (!ok) { - // beginTransaction is called before the try block so we must release the lock in - // the case of failure. - unlockForced(); - } - } - } - - /** - * End a transaction. See beginTransaction for notes about how to use this and when transactions - * are committed and rolled back. - * - * @throws IllegalStateException if the database is not open or is not locked by the current thread - */ - public void endTransaction() { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("no transaction pending"); - } - try { - if (mInnerTransactionIsSuccessful) { - mInnerTransactionIsSuccessful = false; - } else { - mTransactionIsSuccessful = false; - } - if (mLock.getHoldCount() != 1) { - return; - } - RuntimeException savedException = null; - if (mTransactionListener != null) { - try { - if (mTransactionIsSuccessful) { - mTransactionListener.onCommit(); - } else { - mTransactionListener.onRollback(); - } - } catch (RuntimeException e) { - savedException = e; - mTransactionIsSuccessful = false; - } - } - if (mTransactionIsSuccessful) { - execSQL(COMMIT_SQL); - } else { - try { - execSQL("ROLLBACK;"); - if (savedException != null) { - throw savedException; - } - } catch (SQLException e) { - if (Config.LOGD) { - Log.d(TAG, "exception during rollback, maybe the DB previously " - + "performed an auto-rollback"); - } - } - } - } finally { - mTransactionListener = null; - unlockForced(); - if (Config.LOGV) { - Log.v(TAG, "unlocked " + Thread.currentThread() - + ", holdCount is " + mLock.getHoldCount()); - } - } - } - - /** - * Marks the current transaction as successful. Do not do any more database work between - * calling this and calling endTransaction. Do as little non-database work as possible in that - * situation too. If any errors are encountered between this and endTransaction the transaction - * will still be committed. - * - * @throws IllegalStateException if the database is not open, the current thread is not in a transaction, - * or the transaction is already marked as successful. - */ - public void setTransactionSuccessful() { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("no transaction pending"); - } - if (mInnerTransactionIsSuccessful) { - throw new IllegalStateException( - "setTransactionSuccessful may only be called once per call to beginTransaction"); - } - mInnerTransactionIsSuccessful = true; - } - - /** - * return true if there is a transaction pending - */ - public boolean inTransaction() { - return mLock.getHoldCount() > 0; - } - - /** - * Checks if the database lock is held by this thread. - * - * @return true, if this thread is holding the database lock. - */ - public boolean isDbLockedByCurrentThread() { - return mLock.isHeldByCurrentThread(); - } - - /** - * Checks if the database is locked by another thread. This is - * just an estimate, since this status can change at any time, - * including after the call is made but before the result has - * been acted upon. - * - * @return true if the transaction was yielded, false if queue was empty or database was not open - */ - public boolean isDbLockedByOtherThreads() { - return !mLock.isHeldByCurrentThread() && mLock.isLocked(); - } - - /** - * Temporarily end the transaction to let other threads run. The transaction is assumed to be - * successful so far. Do not call setTransactionSuccessful before calling this. When this - * returns a new transaction will have been created but not marked as successful. - * - * @return true if the transaction was yielded - * - * @deprecated if the db is locked more than once (becuase of nested transactions) then the lock - * will not be yielded. Use yieldIfContendedSafely instead. - */ - @Deprecated - public boolean yieldIfContended() { - /* safeguard: */ - if (!isOpen()) return false; - - return yieldIfContendedHelper(false /* do not check yielding */, - -1 /* sleepAfterYieldDelay */); - } - - /** - * Temporarily end the transaction to let other threads run. The transaction is assumed to be - * successful so far. Do not call setTransactionSuccessful before calling this. When this - * returns a new transaction will have been created but not marked as successful. This assumes - * that there are no nested transactions (beginTransaction has only been called once) and will - * throw an exception if that is not the case. - * - * @return true if the transaction was yielded, false if queue was empty or database was not open - */ - public boolean yieldIfContendedSafely() { - /* safeguard: */ - if (!isOpen()) return false; - - return yieldIfContendedHelper(true /* check yielding */, -1 /* sleepAfterYieldDelay*/); - } - - /** - * Temporarily end the transaction to let other threads run. The transaction is assumed to be - * successful so far. Do not call setTransactionSuccessful before calling this. When this - * returns a new transaction will have been created but not marked as successful. This assumes - * that there are no nested transactions (beginTransaction has only been called once) and will - * throw an exception if that is not the case. - * - * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if - * the lock was actually yielded. This will allow other background threads to make some - * more progress than they would if we started the transaction immediately. - * - * @return true if the transaction was yielded, false if queue was empty or database was not open - * - * @throws IllegalStateException if the database is locked more than once by the current thread - * @throws InterruptedException if the thread was interrupted while sleeping - */ - public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { - /* safeguard: */ - if (!isOpen()) return false; - - return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay); - } - - private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) { - if (mLock.getQueueLength() == 0) { - // Reset the lock acquire time since we know that the thread was willing to yield - // the lock at this time. - mLockAcquiredWallTime = SystemClock.elapsedRealtime(); - mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); - return false; - } - setTransactionSuccessful(); - SQLiteTransactionListener transactionListener = mTransactionListener; - endTransaction(); - if (checkFullyYielded) { - if (this.isDbLockedByCurrentThread()) { - throw new IllegalStateException( - "Db locked more than once. yielfIfContended cannot yield"); - } - } - if (sleepAfterYieldDelay > 0) { - // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to - // check if anyone is using the database. If the database is not contended, - // retake the lock and return. - long remainingDelay = sleepAfterYieldDelay; - while (remainingDelay > 0) { - try { - Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ? - remainingDelay : SLEEP_AFTER_YIELD_QUANTUM); - } catch (InterruptedException e) { - Thread.interrupted(); - } - remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM; - if (mLock.getQueueLength() == 0) { - break; - } - } - } - beginTransactionWithListener(transactionListener); - return true; - } - - /** Maps table names to info about what to which _sync_time column to set - * to NULL on an update. This is used to support syncing. */ - private final Map mSyncUpdateInfo = - new HashMap(); - - public Map getSyncedTables() { - synchronized(mSyncUpdateInfo) { - HashMap tables = new HashMap(); - for (String table : mSyncUpdateInfo.keySet()) { - SyncUpdateInfo info = mSyncUpdateInfo.get(table); - if (info.deletedTable != null) { - tables.put(table, info.deletedTable); - } - } - return tables; - } - } - - /** - * Internal class used to keep track what needs to be marked as changed - * when an update occurs. This is used for syncing, so the sync engine - * knows what data has been updated locally. - */ - static private class SyncUpdateInfo { - /** - * Creates the SyncUpdateInfo class. - * - * @param masterTable The table to set _sync_time to NULL in - * @param deletedTable The deleted table that corresponds to the - * master table - * @param foreignKey The key that refers to the primary key in table - */ - SyncUpdateInfo(String masterTable, String deletedTable, - String foreignKey) { - this.masterTable = masterTable; - this.deletedTable = deletedTable; - this.foreignKey = foreignKey; - } - - /** The table containing the _sync_time column */ - String masterTable; - - /** The deleted table that corresponds to the master table */ - String deletedTable; - - /** The key in the local table the row in table. It may be _id, if table - * is the local table. */ - String foreignKey; - } - - /** - * Used to allow returning sub-classes of {@link Cursor} when calling query. - */ - public interface CursorFactory { - /** - * See - * {@link SQLiteCursor#SQLiteCursor(SQLiteDatabase, SQLiteCursorDriver, - * String, SQLiteQuery)}. - */ - public Cursor newCursor(SQLiteDatabase db, - SQLiteCursorDriver masterQuery, String editTable, - SQLiteQuery query); - } - - /** - * Open the database according to the flags {@link #OPEN_READWRITE} - * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. - * - *

Sets the locale of the database to the the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param path to database file to open and/or create - * @param password to use to open and/or create database file - * @param factory an optional factory class that is called to instantiate a - * cursor when query is called, or null for default - * @param flags to control database access mode and other options - * - * @return the newly opened database - * - * @throws SQLiteException if the database cannot be opened - * @throws IllegalArgumentException if the database path is null - */ - public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags) { - return openDatabase(path, password, factory, flags, null); - } - - /** - * Open the database according to the flags {@link #OPEN_READWRITE} - * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. - * - *

Sets the locale of the database to the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param path to database file to open and/or create - * @param password to use to open and/or create database file (char array) - * @param factory an optional factory class that is called to instantiate a - * cursor when query is called, or null for default - * @param flags to control database access mode and other options - * - * @return the newly opened database - * - * @throws SQLiteException if the database cannot be opened - * @throws IllegalArgumentException if the database path is null - */ - public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags) { - return openDatabase(path, password, factory, flags, null, null); - } - - /** - * Open the database according to the flags {@link #OPEN_READWRITE} - * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} - * with optional hook to run on pre/post key events. - * - *

Sets the locale of the database to the the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param path to database file to open and/or create - * @param password to use to open and/or create database file - * @param factory an optional factory class that is called to instantiate a - * cursor when query is called, or null for default - * @param flags to control database access mode and other options - * @param hook to run on pre/post key events - * - * @return the newly opened database - * - * @throws SQLiteException if the database cannot be opened - * @throws IllegalArgumentException if the database path is null - */ - public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags, SQLiteDatabaseHook hook) { - return openDatabase(path, password, factory, flags, hook, null); - } - - /** - * Open the database according to the flags {@link #OPEN_READWRITE} - * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} - * with optional hook to run on pre/post key events. - * - *

Sets the locale of the database to the the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param path to database file to open and/or create - * @param password to use to open and/or create database file (char array) - * @param factory an optional factory class that is called to instantiate a - * cursor when query is called, or null for default - * @param flags to control database access mode and other options - * @param hook to run on pre/post key events (may be null) - * - * @return the newly opened database - * - * @throws SQLiteException if the database cannot be opened - * @throws IllegalArgumentException if the database path is null - */ - public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook hook) { - return openDatabase(path, password, factory, flags, hook, null); - } - - /** - * Open the database according to the flags {@link #OPEN_READWRITE} - * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} - * with optional hook to run on pre/post key events. - * - *

Sets the locale of the database to the the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param path to database file to open and/or create - * @param password to use to open and/or create database file - * @param factory an optional factory class that is called to instantiate a - * cursor when query is called, or null for default - * @param flags to control database access mode and other options - * @param hook to run on pre/post key events - * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database - * corruption (or null for default). - * - * @return the newly opened database - * - * @throws SQLiteException if the database cannot be opened - * @throws IllegalArgumentException if the database path is null - */ - public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory, int flags, - SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) { - return openDatabase(path, password == null ? null : password.toCharArray(), factory, flags, hook, errorHandler); - } - - /** - * Open the database according to the flags {@link #OPEN_READWRITE} - * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS} - * with optional hook to run on pre/post key events. - * - *

Sets the locale of the database to the the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param path to database file to open and/or create - * @param password to use to open and/or create database file (char array) - * @param factory an optional factory class that is called to instantiate a - * cursor when query is called, or null for default - * @param flags to control database access mode and other options - * @param hook to run on pre/post key events (may be null) - * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database - * corruption (or null for default). - * - * @return the newly opened database - * - * @throws SQLiteException if the database cannot be opened - * @throws IllegalArgumentException if the database path is null - */ - public static SQLiteDatabase openDatabase(String path, char[] password, CursorFactory factory, int flags, - SQLiteDatabaseHook hook, DatabaseErrorHandler errorHandler) { - SQLiteDatabase sqliteDatabase = null; - DatabaseErrorHandler myErrorHandler = (errorHandler != null) ? errorHandler : new DefaultDatabaseErrorHandler(); - - try { - // Open the database. - sqliteDatabase = new SQLiteDatabase(path, factory, flags, myErrorHandler); - sqliteDatabase.openDatabaseInternal(password, hook); - } catch (SQLiteDatabaseCorruptException e) { - // Try to recover from this, if possible. - // FUTURE TBD: should we consider this for other open failures? - Log.e(TAG, "Calling error handler for corrupt database " + path, e); - - // NOTE: if this errorHandler.onCorruption() throws the exception _should_ - // bubble back to the original caller. - // DefaultDatabaseErrorHandler deletes the corrupt file, EXCEPT for memory database - myErrorHandler.onCorruption(sqliteDatabase); - - // try *once* again: - sqliteDatabase = new SQLiteDatabase(path, factory, flags, myErrorHandler); - sqliteDatabase.openDatabaseInternal(password, hook); - } - - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - sqliteDatabase.enableSqlTracing(path); - } - if (SQLiteDebug.DEBUG_SQL_TIME) { - sqliteDatabase.enableSqlProfiling(path); - } - - synchronized (sActiveDatabases) { - sActiveDatabases.put(sqliteDatabase, null); - } - - return sqliteDatabase; - } - - /** - * Equivalent to openDatabase(file.getPath(), password, factory, CREATE_IF_NECESSARY, databaseHook). - */ - public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { - return openOrCreateDatabase(file, password, factory, databaseHook, null); - } - - /** - * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook). - */ - public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { - return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook); - } - - /** - * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook). - */ - public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook, - DatabaseErrorHandler errorHandler) { - return openOrCreateDatabase(file == null ? null : file.getPath(), password, factory, databaseHook, errorHandler); - } - - public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory, SQLiteDatabaseHook databaseHook, - DatabaseErrorHandler errorHandler) { - return openDatabase(path, password == null ? null : password.toCharArray(), factory, CREATE_IF_NECESSARY, databaseHook, errorHandler); - } - - public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook) { - return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook); - } - - public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory, SQLiteDatabaseHook databaseHook, - DatabaseErrorHandler errorHandler) { - return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook, errorHandler); - } - - /** - * Equivalent to openDatabase(file.getPath(), password, factory, CREATE_IF_NECESSARY). - */ - public static SQLiteDatabase openOrCreateDatabase(File file, String password, CursorFactory factory) { - return openOrCreateDatabase(file, password, factory, null); - } - - /** - * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY). - */ - public static SQLiteDatabase openOrCreateDatabase(String path, String password, CursorFactory factory) { - return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null); - } - - /** - * Equivalent to openDatabase(path, password, factory, CREATE_IF_NECESSARY). - */ - public static SQLiteDatabase openOrCreateDatabase(String path, char[] password, CursorFactory factory) { - return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null); - } - - /** - * Create a memory backed SQLite database. Its contents will be destroyed - * when the database is closed. - * - *

Sets the locale of the database to the the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param factory an optional factory class that is called to instantiate a - * cursor when query is called - * @param password to use to open and/or create database file - * - * @return a SQLiteDatabase object, or null if the database can't be created - * - * @throws SQLiteException if the database cannot be opened - */ - public static SQLiteDatabase create(CursorFactory factory, String password) { - // This is a magic string with special meaning for SQLite. - return openDatabase(MEMORY, password == null ? null : password.toCharArray(), factory, CREATE_IF_NECESSARY); - } - - /** - * Create a memory backed SQLite database. Its contents will be destroyed - * when the database is closed. - * - *

Sets the locale of the database to the the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param factory an optional factory class that is called to instantiate a - * cursor when query is called - * @param password to use to open and/or create database file (char array) - * - * @return a SQLiteDatabase object, or null if the database can't be created - * - * @throws SQLiteException if the database cannot be opened - */ - public static SQLiteDatabase create(CursorFactory factory, char[] password) { - return openDatabase(MEMORY, password, factory, CREATE_IF_NECESSARY); - } - - - /** - * Close the database. - */ - public void close() { - - if (!isOpen()) { - return; // already closed - } - lock(); - try { - closeClosable(); - // close this database instance - regardless of its reference count value - onAllReferencesReleased(); - } finally { - unlock(); - } - } - - private void closeClosable() { - /* deallocate all compiled sql statement objects from mCompiledQueries cache. - * this should be done before de-referencing all {@link SQLiteClosable} objects - * from this database object because calling - * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database - * to be closed. sqlite doesn't let a database close if there are - * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries. - */ - deallocCachedSqlStatements(); - - Iterator> iter = mPrograms.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry entry = iter.next(); - SQLiteClosable program = entry.getKey(); - if (program != null) { - program.onAllReferencesReleasedFromContainer(); - } - } - } - - /** - * Native call to close the database. - */ - private native void dbclose(); - - /** - * Gets the database version. - * - * @return the database version - * - * @throws IllegalStateException if the database is not open - */ - public int getVersion() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, "PRAGMA user_version;"); - long version = prog.simpleQueryForLong(); - return (int) version; - } finally { - if (prog != null) prog.close(); - unlock(); - } - } - - /** - * Sets the database version. - * - * @param version the new database version - * - * @throws SQLiteException if there is an issue executing the sql internally - * @throws IllegalStateException if the database is not open - */ - public void setVersion(int version) { - execSQL("PRAGMA user_version = " + version); - } - - /** - * Returns the maximum size the database may grow to. - * - * @return the new maximum database size - */ - public long getMaximumSize() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, - "PRAGMA max_page_count;"); - long pageCount = prog.simpleQueryForLong(); - return pageCount * getPageSize(); - } finally { - if (prog != null) prog.close(); - unlock(); - } - } - - /** - * Sets the maximum size the database will grow to. The maximum size cannot - * be set below the current size. - * - * @param numBytes the maximum database size, in bytes - * @return the new maximum database size - */ - public long setMaximumSize(long numBytes) { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - long pageSize = getPageSize(); - long numPages = numBytes / pageSize; - // If numBytes isn't a multiple of pageSize, bump up a page - if ((numBytes % pageSize) != 0) { - numPages++; - } - prog = new SQLiteStatement(this, - "PRAGMA max_page_count = " + numPages); - long newPageCount = prog.simpleQueryForLong(); - return newPageCount * pageSize; - } finally { - if (prog != null) prog.close(); - unlock(); - } - } - - /** - * Returns the current database page size, in bytes. - * - * @return the database page size, in bytes - */ - public long getPageSize() { - SQLiteStatement prog = null; - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - prog = new SQLiteStatement(this, - "PRAGMA page_size;"); - long size = prog.simpleQueryForLong(); - return size; - } finally { - if (prog != null) prog.close(); - unlock(); - } - } - - /** - * Sets the database page size. The page size must be a power of two. This - * method does not work if any data has been written to the database file, - * and must be called right after the database has been created. - * - * @param numBytes the database page size, in bytes - */ - public void setPageSize(long numBytes) { - execSQL("PRAGMA page_size = " + numBytes); - } - - /** - * Mark this table as syncable. When an update occurs in this table the - * _sync_dirty field will be set to ensure proper syncing operation. - * - * @param table the table to mark as syncable - * @param deletedTable The deleted table that corresponds to the - * syncable table - * - * @throws SQLiteException if there is an issue executing the sql to mark the table as syncable - * OR if the database is not open - * - * FUTURE @todo throw IllegalStateException if the database is not open and - * update the test suite - * - * NOTE: This method was deprecated by the AOSP in Android API 11. - */ - public void markTableSyncable(String table, String deletedTable) { - /* safeguard: */ - if (!isOpen()) { - throw new SQLiteException("database not open"); - } - - markTableSyncable(table, "_id", table, deletedTable); - } - - /** - * Mark this table as syncable, with the _sync_dirty residing in another - * table. When an update occurs in this table the _sync_dirty field of the - * row in updateTable with the _id in foreignKey will be set to - * ensure proper syncing operation. - * - * @param table an update on this table will trigger a sync time removal - * @param foreignKey this is the column in table whose value is an _id in - * updateTable - * @param updateTable this is the table that will have its _sync_dirty - * - * @throws SQLiteException if there is an issue executing the sql to mark the table as syncable - * - * FUTURE @todo throw IllegalStateException if the database is not open and - * update the test suite - * - * NOTE: This method was deprecated by the AOSP in Android API 11. - */ - public void markTableSyncable(String table, String foreignKey, - String updateTable) { - /* safeguard: */ - if (!isOpen()) { - throw new SQLiteException("database not open"); - } - - markTableSyncable(table, foreignKey, updateTable, null); - } - - /** - * Mark this table as syncable, with the _sync_dirty residing in another - * table. When an update occurs in this table the _sync_dirty field of the - * row in updateTable with the _id in foreignKey will be set to - * ensure proper syncing operation. - * - * @param table an update on this table will trigger a sync time removal - * @param foreignKey this is the column in table whose value is an _id in - * updateTable - * @param updateTable this is the table that will have its _sync_dirty - * @param deletedTable The deleted table that corresponds to the - * updateTable - * - * @throws SQLiteException if there is an issue executing the sql - */ - private void markTableSyncable(String table, String foreignKey, - String updateTable, String deletedTable) { - lock(); - try { - native_execSQL("SELECT _sync_dirty FROM " + updateTable - + " LIMIT 0"); - native_execSQL("SELECT " + foreignKey + " FROM " + table - + " LIMIT 0"); - } finally { - unlock(); - } - - SyncUpdateInfo info = new SyncUpdateInfo(updateTable, deletedTable, - foreignKey); - synchronized (mSyncUpdateInfo) { - mSyncUpdateInfo.put(table, info); - } - } - - /** - * Call for each row that is updated in a cursor. - * - * @param table the table the row is in - * @param rowId the row ID of the updated row - */ - /* package */ void rowUpdated(String table, long rowId) { - SyncUpdateInfo info; - synchronized (mSyncUpdateInfo) { - info = mSyncUpdateInfo.get(table); - } - if (info != null) { - execSQL("UPDATE " + info.masterTable - + " SET _sync_dirty=1 WHERE _id=(SELECT " + info.foreignKey - + " FROM " + table + " WHERE _id=" + rowId + ")"); - } - } - - /** - * Finds the name of the first table, which is editable. - * - * @param tables a list of tables - * @return the first table listed - */ - public static String findEditTable(String tables) { - if (!TextUtils.isEmpty(tables)) { - // find the first word terminated by either a space or a comma - int spacepos = tables.indexOf(' '); - int commapos = tables.indexOf(','); - - if (spacepos > 0 && (spacepos < commapos || commapos < 0)) { - return tables.substring(0, spacepos); - } else if (commapos > 0 && (commapos < spacepos || spacepos < 0) ) { - return tables.substring(0, commapos); - } - return tables; - } else { - throw new IllegalStateException("Invalid tables"); - } - } - - /** - * Compiles an SQL statement into a reusable pre-compiled statement object. - * The parameters are identical to {@link #execSQL(String)}. You may put ?s in the - * statement and fill in those values with {@link SQLiteProgram#bindString} - * and {@link SQLiteProgram#bindLong} each time you want to run the - * statement. Statements may not return result sets larger than 1x1. - * - * @param sql The raw SQL statement, may contain ? for unknown values to be - * bound later. - * - * @return A pre-compiled {@link SQLiteStatement} object. Note that - * {@link SQLiteStatement}s are not synchronized, see the documentation for more details. - * - * @throws SQLException If the SQL string is invalid for some reason - * @throws IllegalStateException if the database is not open - */ - public SQLiteStatement compileStatement(String sql) throws SQLException { - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - return new SQLiteStatement(this, sql); - } finally { - unlock(); - } - } - - /** - * Query the given URL, returning a {@link Cursor} over the result set. - * - * @param distinct true if you want each row to be unique, false otherwise. - * @param table The table name to compile the query against. - * @param columns A list of which columns to return. Passing null will - * return all columns, which is discouraged to prevent reading - * data from storage that isn't going to be used. - * @param selection A filter declaring which rows to return, formatted as an - * SQL WHERE clause (excluding the WHERE itself). Passing null - * will return all rows for the given table. - * @param selectionArgs You may include ?s in selection, which will be - * replaced by the values from selectionArgs, in order that they - * appear in the selection. The values will be bound as Strings. - * @param groupBy A filter declaring how to group rows, formatted as an SQL - * GROUP BY clause (excluding the GROUP BY itself). Passing null - * will cause the rows to not be grouped. - * @param having A filter declare which row groups to include in the cursor, - * if row grouping is being used, formatted as an SQL HAVING - * clause (excluding the HAVING itself). Passing null will cause - * all row groups to be included, and is required when row - * grouping is not being used. - * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause - * (excluding the ORDER BY itself). Passing null will use the - * default sort order, which may be unordered. - * @param limit Limits the number of rows returned by the query, - * formatted as LIMIT clause. Passing null denotes no LIMIT clause. - * - * @return A {@link Cursor} object, which is positioned before the first entry. Note that - * {@link Cursor}s are not synchronized, see the documentation for more details. - * - * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid - * @throws IllegalStateException if the database is not open - * - * @see Cursor - */ - public Cursor query(boolean distinct, String table, String[] columns, - String selection, String[] selectionArgs, String groupBy, - String having, String orderBy, String limit) { - return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, - groupBy, having, orderBy, limit); - } - - /** - * Query the given URL, returning a {@link Cursor} over the result set. - * - * @param cursorFactory the cursor factory to use, or null for the default factory - * @param distinct true if you want each row to be unique, false otherwise. - * @param table The table name to compile the query against. - * @param columns A list of which columns to return. Passing null will - * return all columns, which is discouraged to prevent reading - * data from storage that isn't going to be used. - * @param selection A filter declaring which rows to return, formatted as an - * SQL WHERE clause (excluding the WHERE itself). Passing null - * will return all rows for the given table. - * @param selectionArgs You may include ?s in selection, which will be - * replaced by the values from selectionArgs, in order that they - * appear in the selection. The values will be bound as Strings. - * @param groupBy A filter declaring how to group rows, formatted as an SQL - * GROUP BY clause (excluding the GROUP BY itself). Passing null - * will cause the rows to not be grouped. - * @param having A filter declare which row groups to include in the cursor, - * if row grouping is being used, formatted as an SQL HAVING - * clause (excluding the HAVING itself). Passing null will cause - * all row groups to be included, and is required when row - * grouping is not being used. - * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause - * (excluding the ORDER BY itself). Passing null will use the - * default sort order, which may be unordered. - * @param limit Limits the number of rows returned by the query, - * formatted as LIMIT clause. Passing null denotes no LIMIT clause. - * - * @return A {@link Cursor} object, which is positioned before the first entry. Note that - * {@link Cursor}s are not synchronized, see the documentation for more details. - * - * @see Cursor - */ - public Cursor queryWithFactory(CursorFactory cursorFactory, - boolean distinct, String table, String[] columns, - String selection, String[] selectionArgs, String groupBy, - String having, String orderBy, String limit) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - String sql = SQLiteQueryBuilder.buildQueryString( - distinct, table, columns, selection, groupBy, having, orderBy, limit); - - return rawQueryWithFactory( - cursorFactory, sql, selectionArgs, findEditTable(table)); - } - - /** - * Query the given table, returning a {@link Cursor} over the result set. - * - * @param table The table name to compile the query against. - * @param columns A list of which columns to return. Passing null will - * return all columns, which is discouraged to prevent reading - * data from storage that isn't going to be used. - * @param selection A filter declaring which rows to return, formatted as an - * SQL WHERE clause (excluding the WHERE itself). Passing null - * will return all rows for the given table. - * @param selectionArgs You may include ?s in selection, which will be - * replaced by the values from selectionArgs, in order that they - * appear in the selection. The values will be bound as Strings. - * @param groupBy A filter declaring how to group rows, formatted as an SQL - * GROUP BY clause (excluding the GROUP BY itself). Passing null - * will cause the rows to not be grouped. - * @param having A filter declare which row groups to include in the cursor, - * if row grouping is being used, formatted as an SQL HAVING - * clause (excluding the HAVING itself). Passing null will cause - * all row groups to be included, and is required when row - * grouping is not being used. - * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause - * (excluding the ORDER BY itself). Passing null will use the - * default sort order, which may be unordered. - * - * @return A {@link Cursor} object, which is positioned before the first entry. Note that - * {@link Cursor}s are not synchronized, see the documentation for more details. - * - * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid - * @throws IllegalStateException if the database is not open - * - * @see Cursor - */ - public Cursor query(String table, String[] columns, String selection, - String[] selectionArgs, String groupBy, String having, - String orderBy) { - - return query(false, table, columns, selection, selectionArgs, groupBy, - having, orderBy, null /* limit */); - } - - /** - * Query the given table, returning a {@link Cursor} over the result set. - * - * @param table The table name to compile the query against. - * @param columns A list of which columns to return. Passing null will - * return all columns, which is discouraged to prevent reading - * data from storage that isn't going to be used. - * @param selection A filter declaring which rows to return, formatted as an - * SQL WHERE clause (excluding the WHERE itself). Passing null - * will return all rows for the given table. - * @param selectionArgs You may include ?s in selection, which will be - * replaced by the values from selectionArgs, in order that they - * appear in the selection. The values will be bound as Strings. - * @param groupBy A filter declaring how to group rows, formatted as an SQL - * GROUP BY clause (excluding the GROUP BY itself). Passing null - * will cause the rows to not be grouped. - * @param having A filter declare which row groups to include in the cursor, - * if row grouping is being used, formatted as an SQL HAVING - * clause (excluding the HAVING itself). Passing null will cause - * all row groups to be included, and is required when row - * grouping is not being used. - * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause - * (excluding the ORDER BY itself). Passing null will use the - * default sort order, which may be unordered. - * @param limit Limits the number of rows returned by the query, - * formatted as LIMIT clause. Passing null denotes no LIMIT clause. - * - * @return A {@link Cursor} object, which is positioned before the first entry. Note that - * {@link Cursor}s are not synchronized, see the documentation for more details. - * - * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid - * @throws IllegalStateException if the database is not open - * - * @see Cursor - */ - public Cursor query(String table, String[] columns, String selection, - String[] selectionArgs, String groupBy, String having, - String orderBy, String limit) { - - return query(false, table, columns, selection, selectionArgs, groupBy, - having, orderBy, limit); - } - - /** - * Runs the provided SQL and returns a {@link Cursor} over the result set. - * - * @param sql the SQL query. The SQL string must not be ; terminated - * @param selectionArgs You may include ?s in where clause in the query, - * which will be replaced by the values from selectionArgs. The - * values will be bound as Strings. - * - * @return A {@link Cursor} object, which is positioned before the first entry. Note that - * {@link Cursor}s are not synchronized, see the documentation for more details. - * - * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid - * @throws IllegalStateException if the database is not open - */ - public Cursor rawQuery(String sql, String[] selectionArgs) { - return rawQueryWithFactory(null, sql, selectionArgs, null); - } - - - /** - * Runs the provided SQL and returns a {@link Cursor} over the result set. - * - * @param sql the SQL query. The SQL string must not be ; terminated - * @param args You may include ?s in where clause in the query, - * which will be replaced by the values from args. The - * values will be bound by their type. - * - * @return A {@link Cursor} object, which is positioned before the first entry. Note that - * {@link Cursor}s are not synchronized, see the documentation for more details. - * - * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid - * @throws IllegalStateException if the database is not open - */ - public Cursor rawQuery(String sql, Object[] args) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - long timeStart = 0; - if (Config.LOGV || mSlowQueryThreshold != -1) { - timeStart = System.currentTimeMillis(); - } - SQLiteDirectCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, null); - Cursor cursor = null; - try { - cursor = driver.query(mFactory, args); - } finally { - if (Config.LOGV || mSlowQueryThreshold != -1) { - // Force query execution - int count = -1; - if (cursor != null) { - count = cursor.getCount(); - } - - long duration = System.currentTimeMillis() - timeStart; - - if (Config.LOGV || duration >= mSlowQueryThreshold) { - Log.v(TAG, - "query (" + duration + " ms): " + driver.toString() + - ", args are , count is " + count); - } - } - } - return new CrossProcessCursorWrapper(cursor); - } - - /** - * Runs the provided SQL and returns a cursor over the result set. - * - * @param cursorFactory the cursor factory to use, or null for the default factory - * @param sql the SQL query. The SQL string must not be ; terminated - * @param selectionArgs You may include ?s in where clause in the query, - * which will be replaced by the values from selectionArgs. The - * values will be bound as Strings. - * @param editTable the name of the first table, which is editable - * - * @return A {@link Cursor} object, which is positioned before the first entry. Note that - * {@link Cursor}s are not synchronized, see the documentation for more details. - * - * @throws SQLiteException if there is an issue executing the sql or the SQL string is invalid - * @throws IllegalStateException if the database is not open - */ - public Cursor rawQueryWithFactory( - CursorFactory cursorFactory, String sql, String[] selectionArgs, - String editTable) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - long timeStart = 0; - - if (Config.LOGV || mSlowQueryThreshold != -1) { - timeStart = System.currentTimeMillis(); - } - - SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable); - - Cursor cursor = null; - try { - cursor = driver.query( - cursorFactory != null ? cursorFactory : mFactory, - selectionArgs); - } finally { - if (Config.LOGV || mSlowQueryThreshold != -1) { - - // Force query execution - int count = -1; - if (cursor != null) { - count = cursor.getCount(); - } - - long duration = System.currentTimeMillis() - timeStart; - - if (Config.LOGV || duration >= mSlowQueryThreshold) { - Log.v(TAG, - "query (" + duration + " ms): " + driver.toString() + - ", args are , count is " + count); - } - } - } - return new CrossProcessCursorWrapper(cursor); - } - - /** - * Runs the provided SQL and returns a cursor over the result set. - * The cursor will read an initial set of rows and the return to the caller. - * It will continue to read in batches and send data changed notifications - * when the later batches are ready. - * @param sql the SQL query. The SQL string must not be ; terminated - * @param selectionArgs You may include ?s in where clause in the query, - * which will be replaced by the values from selectionArgs. The - * values will be bound as Strings. - * @param initialRead set the initial count of items to read from the cursor - * @param maxRead set the count of items to read on each iteration after the first - * @return A {@link Cursor} object, which is positioned before the first entry. Note that - * {@link Cursor}s are not synchronized, see the documentation for more details. - * - * This work is incomplete and not fully tested or reviewed, so currently - * hidden. - * @hide - */ - public Cursor rawQuery(String sql, String[] selectionArgs, - int initialRead, int maxRead) { - net.sqlcipher.CursorWrapper cursorWrapper = (net.sqlcipher.CursorWrapper)rawQueryWithFactory(null, sql, selectionArgs, null); - ((SQLiteCursor)cursorWrapper.getWrappedCursor()).setLoadStyle(initialRead, maxRead); - return cursorWrapper; - } - - /** - * Convenience method for inserting a row into the database. - * - * @param table the table to insert the row into - * @param nullColumnHack SQL doesn't allow inserting a completely empty row, - * so if initialValues is empty this column will explicitly be - * assigned a NULL value - * @param values this map contains the initial column values for the - * row. The keys should be the column names and the values the - * column values - * @return the row ID of the newly inserted row, or -1 if an error occurred - */ - 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 into " + table, e); - return -1; - } - } - - /** - * Convenience method for inserting a row into the database. - * - * @param table the table to insert the row into - * @param nullColumnHack SQL doesn't allow inserting a completely empty row, - * so if initialValues is empty this column will explicitly be - * assigned a NULL value - * @param values this map contains the initial column values for the - * row. The keys should be the column names and the values the - * column values - * @throws SQLException - * @return the row ID of the newly inserted row, or -1 if an error occurred - */ - public long insertOrThrow(String table, String nullColumnHack, ContentValues values) - throws SQLException { - return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); - } - - /** - * Convenience method for replacing a row in the database. - * - * @param table the table in which to replace the row - * @param nullColumnHack SQL doesn't allow inserting a completely empty row, - * so if initialValues is empty this row will explicitly be - * assigned a NULL value - * @param initialValues this map contains the initial column values for - * the row. The key - * @return the row ID of the newly inserted row, or -1 if an error occurred - */ - public long replace(String table, String nullColumnHack, ContentValues initialValues) { - try { - return insertWithOnConflict(table, nullColumnHack, initialValues, - CONFLICT_REPLACE); - } catch (SQLException e) { - Log.e(TAG, "Error inserting into " + table, e); - return -1; - } - } - - /** - * Convenience method for replacing a row in the database. - * - * @param table the table in which to replace the row - * @param nullColumnHack SQL doesn't allow inserting a completely empty row, - * so if initialValues is empty this row will explicitly be - * assigned a NULL value - * @param initialValues this map contains the initial column values for - * the row. The key - * @throws SQLException - * @return the row ID of the newly inserted row, or -1 if an error occurred - */ - public long replaceOrThrow(String table, String nullColumnHack, - ContentValues initialValues) throws SQLException { - return insertWithOnConflict(table, nullColumnHack, initialValues, - CONFLICT_REPLACE); - } - - /** - * General method for inserting a row into the database. - * - * @param table the table to insert the row into - * @param nullColumnHack SQL doesn't allow inserting a completely empty row, - * so if initialValues is empty this column will explicitly be - * assigned a NULL value - * @param initialValues this map contains the initial column values for the - * row. The keys should be the column names and the values the - * column values - * @param conflictAlgorithm for insert conflict resolver - * - * @return the row ID of the newly inserted row - * OR the primary key of the existing row if the input param 'conflictAlgorithm' = - * {@link #CONFLICT_IGNORE} - * OR -1 if any error - * - * @throws SQLException If the SQL string is invalid for some reason - * @throws IllegalStateException if the database is not open - */ - public long insertWithOnConflict(String table, String nullColumnHack, - ContentValues initialValues, int conflictAlgorithm) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - - // Measurements show most sql lengths <= 152 - StringBuilder sql = new StringBuilder(152); - sql.append("INSERT"); - sql.append(CONFLICT_VALUES[conflictAlgorithm]); - sql.append(" INTO "); - sql.append(table); - // Measurements show most values lengths < 40 - StringBuilder values = new StringBuilder(40); - - Set> entrySet = null; - if (initialValues != null && initialValues.size() > 0) { - entrySet = initialValues.valueSet(); - Iterator> entriesIter = entrySet.iterator(); - sql.append('('); - - boolean needSeparator = false; - while (entriesIter.hasNext()) { - if (needSeparator) { - sql.append(", "); - values.append(", "); - } - needSeparator = true; - Map.Entry entry = entriesIter.next(); - sql.append(entry.getKey()); - values.append('?'); - } - - sql.append(')'); - } else { - sql.append("(" + nullColumnHack + ") "); - values.append("NULL"); - } - - sql.append(" VALUES("); - sql.append(values); - sql.append(");"); - - lock(); - SQLiteStatement statement = null; - try { - statement = compileStatement(sql.toString()); - - // Bind the values - if (entrySet != null) { - int size = entrySet.size(); - Iterator> entriesIter = entrySet.iterator(); - for (int i = 0; i < size; i++) { - Map.Entry entry = entriesIter.next(); - DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue()); - - } - } - - // Run the program and then cleanup - statement.execute(); - - long insertedRowId = lastChangeCount() > 0 ? lastInsertRow() : -1; - if (insertedRowId == -1) { - Log.e(TAG, "Error inserting using into " + table); - } else { - if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Inserting row " + insertedRowId + - " from using into " + table); - } - } - return insertedRowId; - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; - } finally { - if (statement != null) { - statement.close(); - } - unlock(); - } - } - - /** - * Convenience method for deleting rows in the database. - * - * @param table the table to delete from - * @param whereClause the optional WHERE clause to apply when deleting. - * Passing null will delete all rows. - * - * @return the number of rows affected if a whereClause is passed in, 0 - * otherwise. To remove all rows and get a count pass "1" as the - * whereClause. - * - * @throws SQLException If the SQL string is invalid for some reason - * @throws IllegalStateException if the database is not open - */ - public int delete(String table, String whereClause, String[] whereArgs) { - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - SQLiteStatement statement = null; - try { - statement = compileStatement("DELETE FROM " + table - + (!TextUtils.isEmpty(whereClause) - ? " WHERE " + whereClause : "")); - if (whereArgs != null) { - int numArgs = whereArgs.length; - for (int i = 0; i < numArgs; i++) { - DatabaseUtils.bindObjectToProgram(statement, i + 1, whereArgs[i]); - } - } - statement.execute(); - return lastChangeCount(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; - } finally { - if (statement != null) { - statement.close(); - } - unlock(); - } - } - - /** - * Convenience method for updating rows in the database. - * - * @param table the table to update in - * @param values a map from column names to new column values. null is a - * valid value that will be translated to NULL. - * @param whereClause the optional WHERE clause to apply when updating. - * Passing null will update all rows. - * - * @return the number of rows affected - * - * @throws SQLException If the SQL string is invalid for some reason - * @throws IllegalStateException if the database is not open - */ - public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { - return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE); - } - - /** - * Convenience method for updating rows in the database. - * - * @param table the table to update in - * @param values a map from column names to new column values. null is a - * valid value that will be translated to NULL. - * @param whereClause the optional WHERE clause to apply when updating. - * Passing null will update all rows. - * @param conflictAlgorithm for update conflict resolver - * - * @return the number of rows affected - * - * @throws SQLException If the SQL string is invalid for some reason - * @throws IllegalStateException if the database is not open - */ - public int updateWithOnConflict(String table, ContentValues values, - String whereClause, String[] whereArgs, int conflictAlgorithm) { - if (values == null || values.size() == 0) { - throw new IllegalArgumentException("Empty values"); - } - - StringBuilder sql = new StringBuilder(120); - sql.append("UPDATE "); - sql.append(CONFLICT_VALUES[conflictAlgorithm]); - sql.append(table); - sql.append(" SET "); - - Set> entrySet = values.valueSet(); - Iterator> entriesIter = entrySet.iterator(); - - while (entriesIter.hasNext()) { - Map.Entry entry = entriesIter.next(); - sql.append(entry.getKey()); - sql.append("=?"); - if (entriesIter.hasNext()) { - sql.append(", "); - } - } - - if (!TextUtils.isEmpty(whereClause)) { - sql.append(" WHERE "); - sql.append(whereClause); - } - - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - SQLiteStatement statement = null; - try { - statement = compileStatement(sql.toString()); - - // Bind the values - int size = entrySet.size(); - entriesIter = entrySet.iterator(); - int bindArg = 1; - for (int i = 0; i < size; i++) { - Map.Entry entry = entriesIter.next(); - DatabaseUtils.bindObjectToProgram(statement, bindArg, entry.getValue()); - bindArg++; - } - - if (whereArgs != null) { - size = whereArgs.length; - for (int i = 0; i < size; i++) { - statement.bindString(bindArg, whereArgs[i]); - bindArg++; - } - } - - // Run the program and then cleanup - statement.execute(); - int numChangedRows = lastChangeCount(); - if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Updated " + numChangedRows + - " rows using and for " + table); - } - return numChangedRows; - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; - } catch (SQLException e) { - Log.e(TAG, "Error updating using for " + table); - throw e; - } finally { - if (statement != null) { - statement.close(); - } - unlock(); - } - } - - /** - * Execute a single SQL statement that is not a query. For example, CREATE - * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not - * supported. it takes a write lock - * - * @throws SQLException If the SQL string is invalid for some reason - * @throws IllegalStateException if the database is not open - */ - public void execSQL(String sql) throws SQLException { - long timeStart = SystemClock.uptimeMillis(); - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - native_execSQL(sql); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; - } finally { - unlock(); - } - } - - public void rawExecSQL(String sql){ - long timeStart = SystemClock.uptimeMillis(); - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - try { - native_rawExecSQL(sql); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; - } finally { - unlock(); - } - } - - /** - * Execute a single SQL statement that is not a query. For example, CREATE - * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not - * supported. it takes a write lock, - * - * @param sql - * @param bindArgs only byte[], String, Long and Double are supported in bindArgs. - * - * @throws SQLException If the SQL string is invalid for some reason - * @throws IllegalStateException if the database is not open - */ - public void execSQL(String sql, Object[] bindArgs) throws SQLException { - if (bindArgs == null) { - throw new IllegalArgumentException("Empty bindArgs"); - } - long timeStart = SystemClock.uptimeMillis(); - lock(); - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - SQLiteStatement statement = null; - try { - statement = compileStatement(sql); - if (bindArgs != null) { - int numArgs = bindArgs.length; - for (int i = 0; i < numArgs; i++) { - DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]); - } - } - statement.execute(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; - } finally { - if (statement != null) { - statement.close(); - } - unlock(); - } - } - - @Override - protected void finalize() { - if (isOpen()) { - Log.e(TAG, "close() was never explicitly called on database '" + - mPath + "' ", mStackTrace); - closeClosable(); - onAllReferencesReleased(); - } - } - - /** - * Public constructor which attempts to open the database. See {@link #create} and {@link #openDatabase}. - * - *

Sets the locale of the database to the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param path The full path to the database - * @param password to use to open and/or create a database file (char array) - * @param factory The factory to use when creating cursors, may be NULL. - * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already - * exists, mFlags will be updated appropriately. - * - * @throws SQLiteException if the database cannot be opened - * @throws IllegalArgumentException if the database path is null - */ - public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags) { - this(path, factory, flags, null); - this.openDatabaseInternal(password, null); - } - - /** - * Public constructor which attempts to open the database. See {@link #create} and {@link #openDatabase}. - * - *

Sets the locale of the database to the system's current locale. - * Call {@link #setLocale} if you would like something else.

- * - * @param path The full path to the database - * @param password to use to open and/or create a database file (char array) - * @param factory The factory to use when creating cursors, may be NULL. - * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already - * exists, mFlags will be updated appropriately. - * @param databaseHook to run on pre/post key events - * - * @throws SQLiteException if the database cannot be opened - * @throws IllegalArgumentException if the database path is null - */ - public SQLiteDatabase(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook databaseHook) { - this(path, factory, flags, null); - this.openDatabaseInternal(password, databaseHook); - } - - /** - * Private constructor (without database password) which DOES NOT attempt to open the database. - * - * @param path The full path to the database - * @param factory The factory to use when creating cursors, may be NULL. - * @param flags to control database access mode and other options - * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database - * corruption (or null for default). - * - * @throws IllegalArgumentException if the database path is null - */ - private SQLiteDatabase(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) { - if (path == null) { - throw new IllegalArgumentException("path should not be null"); - } - - mFlags = flags; - mPath = path; - - mSlowQueryThreshold = -1;//SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); - mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - mFactory = factory; - mPrograms = new WeakHashMap(); - - mErrorHandler = errorHandler; - } - - private void openDatabaseInternal(final char[] password, SQLiteDatabaseHook hook) { - boolean shouldCloseConnection = true; - final byte[] keyMaterial = getBytes(password); - dbopen(mPath, mFlags); - try { - - keyDatabase(hook, new Runnable() { - public void run() { - if(keyMaterial != null && keyMaterial.length > 0) { - key(keyMaterial); - } - } - }); - shouldCloseConnection = false; - - } catch(RuntimeException ex) { - - if(containsNull(password)) { - keyDatabase(hook, new Runnable() { - public void run() { - if(password != null) { - key_mutf8(password); - } - } - }); - if(keyMaterial != null && keyMaterial.length > 0) { - rekey(keyMaterial); - } - shouldCloseConnection = false; - } else { - throw ex; - } - - } finally { - if(shouldCloseConnection) { - dbclose(); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeClosed = getTime(); - } - } - if(keyMaterial != null && keyMaterial.length > 0) { - for(byte data : keyMaterial) { - data = 0; - } - } - } - - } - - private boolean containsNull(char[] data) { - char defaultValue = '\u0000'; - boolean status = false; - if(data != null && data.length > 0) { - for(char datum : data) { - if(datum == defaultValue) { - status = true; - break; - } - } - } - return status; - } - - private void keyDatabase(SQLiteDatabaseHook databaseHook, Runnable keyOperation) { - if(databaseHook != null) { - databaseHook.preKey(this); - } - if(keyOperation != null){ - keyOperation.run(); - } - if(databaseHook != null){ - databaseHook.postKey(this); - } - if (SQLiteDebug.DEBUG_SQL_CACHE) { - mTimeOpened = getTime(); - } - try { - Cursor cursor = rawQuery("select count(*) from sqlite_master;", new String[]{}); - if(cursor != null){ - cursor.moveToFirst(); - int count = cursor.getInt(0); - cursor.close(); - } - } catch (RuntimeException e) { - Log.e(TAG, e.getMessage(), e); - throw e; - } - } - - private String getTime() { - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ", Locale.US).format(System.currentTimeMillis()); - } - - /** - * return whether the DB is opened as read only. - * @return true if DB is opened as read only - */ - public boolean isReadOnly() { - return (mFlags & OPEN_READ_MASK) == OPEN_READONLY; - } - - /** - * @return true if the DB is currently open (has not been closed) - */ - public boolean isOpen() { - return mNativeHandle != 0; - } - - public boolean needUpgrade(int newVersion) { - /* NOTE: getVersion() will throw if database is not open. */ - return newVersion > getVersion(); - } - - /** - * Getter for the path to the database file. - * - * @return the path to our database file. - */ - public final String getPath() { - return mPath; - } - - /** - * Removes email addresses from database filenames before they're - * logged to the EventLog where otherwise apps could potentially - * read them. - */ - private String getPathForLogs() { - if (mPathForLogs != null) { - return mPathForLogs; - } - if (mPath == null) { - return null; - } - if (mPath.indexOf('@') == -1) { - mPathForLogs = mPath; - } else { - mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY"); - } - return mPathForLogs; - } - - /** - * Sets the locale for this database. Does nothing if this database has - * the NO_LOCALIZED_COLLATORS flag set or was opened read only. - * - * @throws SQLException if the locale could not be set. The most common reason - * for this is that there is no collator available for the locale you requested. - * In this case the database remains unchanged. - */ - public void setLocale(Locale locale) { - lock(); - try { - native_setLocale(locale.toString(), mFlags); - } finally { - unlock(); - } - } - - /* - * ============================================================================ - * - * The following methods deal with compiled-sql cache - * ============================================================================ - */ - /** - * adds the given sql and its compiled-statement-id-returned-by-sqlite to the - * cache of compiledQueries attached to 'this'. - * - * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql, - * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current - * mapping is NOT replaced with the new mapping). - */ - /* package */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) { - if (mMaxSqlCacheSize == 0) { - // for this database, there is no cache of compiled sql. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql); - } - return; - } - - SQLiteCompiledSql compiledSql = null; - synchronized(mCompiledQueries) { - // don't insert the new mapping if a mapping already exists - compiledSql = mCompiledQueries.get(sql); - if (compiledSql != null) { - return; - } - // add this to the cache - if (mCompiledQueries.size() == mMaxSqlCacheSize) { - /* - * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. - * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times - * chances are it is NOT using ? for bindargs - so caching is useless. - * TODO: either let the callers set max cchesize for their app, or intelligently - * figure out what should be cached for a given app. - */ - if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) { - Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + - getPath() + "; i.e., NO space for this sql statement in cache: " + - sql + ". Please change your sql statements to use '?' for " + - "bindargs, instead of using actual values"); - } - // don't add this entry to cache - } else { - // cache is NOT full. add this to cache. - mCompiledQueries.put(sql, compiledStatement); - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + - mCompiledQueries.size() + "|" + sql); - } - } - } - return; - } - - - private void deallocCachedSqlStatements() { - synchronized (mCompiledQueries) { - for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) { - compiledSql.releaseSqlStatement(); - } - mCompiledQueries.clear(); - } - } - - /** - * from the compiledQueries cache, returns the compiled-statement-id for the given sql. - * returns null, if not found in the cache. - */ - /* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) { - SQLiteCompiledSql compiledStatement = null; - boolean cacheHit; - synchronized(mCompiledQueries) { - if (mMaxSqlCacheSize == 0) { - // for this database, there is no cache of compiled sql. - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|cache NOT found|" + getPath()); - } - return null; - } - cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null; - } - if (cacheHit) { - mNumCacheHits++; - } else { - mNumCacheMisses++; - } - - if (SQLiteDebug.DEBUG_SQL_CACHE) { - Log.v(TAG, "|cache_stats|" + - getPath() + "|" + mCompiledQueries.size() + - "|" + mNumCacheHits + "|" + mNumCacheMisses + - "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql); - } - return compiledStatement; - } - - /** - * returns true if the given sql is cached in compiled-sql cache. - * @hide - */ - public boolean isInCompiledSqlCache(String sql) { - synchronized(mCompiledQueries) { - return mCompiledQueries.containsKey(sql); - } - } - - /** - * purges the given sql from the compiled-sql cache. - * @hide - */ - public void purgeFromCompiledSqlCache(String sql) { - synchronized(mCompiledQueries) { - mCompiledQueries.remove(sql); - } - } - - /** - * remove everything from the compiled sql cache - * @hide - */ - public void resetCompiledSqlCache() { - synchronized(mCompiledQueries) { - mCompiledQueries.clear(); - } - } - - /** - * return the current maxCacheSqlCacheSize - * @hide - */ - public synchronized int getMaxSqlCacheSize() { - return mMaxSqlCacheSize; - } - - /** - * set the max size of the compiled sql cache for this database after purging the cache. - * (size of the cache = number of compiled-sql-statements stored in the cache). - * - * max cache size can ONLY be increased from its current size (default = 0). - * if this method is called with smaller size than the current value of mMaxSqlCacheSize, - * then IllegalStateException is thrown - * - * synchronized because we don't want t threads to change cache size at the same time. - * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE) - * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or - * < the value set with previous setMaxSqlCacheSize() call. - * - * @hide - */ - public synchronized void setMaxSqlCacheSize(int cacheSize) { - if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { - throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE); - } else if (cacheSize < mMaxSqlCacheSize) { - throw new IllegalStateException("cannot set cacheSize to a value less than the value " + - "set with previous setMaxSqlCacheSize() call."); - } - mMaxSqlCacheSize = cacheSize; - } - - /** - * this method is used to collect data about ALL open databases in the current process. - * bugreport is a user of this data. - */ - /* package */ static ArrayList getDbStats() { - ArrayList dbStatsList = new ArrayList(); - - for (SQLiteDatabase db : getActiveDatabases()) { - if (db == null || !db.isOpen()) { - continue; - } - - // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db - int lookasideUsed = db.native_getDbLookaside(); - - // get the lastnode of the dbname - String path = db.getPath(); - int indx = path.lastIndexOf("/"); - String lastnode = path.substring((indx != -1) ? ++indx : 0); - - // get list of attached dbs and for each db, get its size and pagesize - ArrayList> attachedDbs = getAttachedDbs(db); - if (attachedDbs == null) { - continue; - } - for (int i = 0; i < attachedDbs.size(); i++) { - Pair p = attachedDbs.get(i); - long pageCount = getPragmaVal(db, p.first + ".page_count;"); - - // first entry in the attached db list is always the main database - // don't worry about prefixing the dbname with "main" - String dbName; - if (i == 0) { - dbName = lastnode; - } else { - // lookaside is only relevant for the main db - lookasideUsed = 0; - dbName = " (attached) " + p.first; - // if the attached db has a path, attach the lastnode from the path to above - if (p.second.trim().length() > 0) { - int idx = p.second.lastIndexOf("/"); - dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0); - } - } - if (pageCount > 0) { - dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), - lookasideUsed)); - } - } - } - return dbStatsList; - } - - private static ArrayList getActiveDatabases() { - ArrayList databases = new ArrayList(); - synchronized (sActiveDatabases) { - databases.addAll(sActiveDatabases.keySet()); - } - return databases; - } - - /** - * get the specified pragma value from sqlite for the specified database. - * only handles pragma's that return int/long. - * NO JAVA locks are held in this method. - * TODO: use this to do all pragma's in this class - */ - private static long getPragmaVal(SQLiteDatabase db, String pragma) { - if (!db.isOpen()) { - return 0; - } - SQLiteStatement prog = null; - try { - prog = new SQLiteStatement(db, "PRAGMA " + pragma); - long val = prog.simpleQueryForLong(); - return val; - } finally { - if (prog != null) prog.close(); - } - } - - /** - * returns list of full pathnames of all attached databases - * including the main database - * TODO: move this to {@link DatabaseUtils} - */ - private static ArrayList> getAttachedDbs(SQLiteDatabase dbObj) { - if (!dbObj.isOpen()) { - return null; - } - ArrayList> attachedDbs = new ArrayList>(); - Cursor c = dbObj.rawQuery("pragma database_list;", null); - while (c.moveToNext()) { - attachedDbs.add(new Pair(c.getString(1), c.getString(2))); - } - c.close(); - return attachedDbs; - } - - private byte[] getBytes(char[] data) { - if(data == null || data.length == 0) return null; - CharBuffer charBuffer = CharBuffer.wrap(data); - ByteBuffer byteBuffer = Charset.forName(KEY_ENCODING).encode(charBuffer); - byte[] result = new byte[byteBuffer.limit()]; - byteBuffer.get(result); - return result; - } - - /** - * Sets the root directory to search for the ICU data file - */ - public static native void setICURoot(String path); - - /** - * Native call to open the database. - * - * @param path The full path to the database - */ - private native void dbopen(String path, int flags); - - /** - * Native call to setup tracing of all sql statements - * - * @param path the full path to the database - */ - private native void enableSqlTracing(String path); - - /** - * Native call to setup profiling of all sql statements. - * currently, sqlite's profiling = printing of execution-time - * (wall-clock time) of each of the sql statements, as they - * are executed. - * - * @param path the full path to the database - */ - private native void enableSqlProfiling(String path); - - /** - * Native call to execute a raw SQL statement. {@link #lock} must be held - * when calling this method. - * - * @param sql The raw SQL string - * - * @throws SQLException - */ - /* package */ native void native_execSQL(String sql) throws SQLException; - - /** - * Native call to set the locale. {@link #lock} must be held when calling - * this method. - * - * @throws SQLException - */ - /* package */ native void native_setLocale(String loc, int flags); - - /** - * Returns the row ID of the last row inserted into the database. - * - * @return the row ID of the last row inserted into the database. - */ - /* package */ native long lastInsertRow(); - - /** - * Returns the number of changes made in the last statement executed. - * - * @return the number of changes made in the last statement executed. - */ - /* package */ native int lastChangeCount(); - - /** - * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here - * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html - * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED - */ - private native int native_getDbLookaside(); - - private native void native_rawExecSQL(String sql); - - private native int native_status(int operation, boolean reset); - - private native void native_key(char[] key) throws SQLException; - - private native void native_rekey(String key) throws SQLException; - - private native void key(byte[] key) throws SQLException; - private native void key_mutf8(char[] key) throws SQLException; - private native void rekey(byte[] key) throws SQLException; -} diff --git a/src/net/sqlcipher/database/SQLiteDatabaseCorruptException.java b/src/net/sqlcipher/database/SQLiteDatabaseCorruptException.java deleted file mode 100644 index 2e7373ca..00000000 --- a/src/net/sqlcipher/database/SQLiteDatabaseCorruptException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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. - */ - -package net.sqlcipher.database; - -/** - * An exception that indicates that the SQLite database file is corrupt. - */ -public class SQLiteDatabaseCorruptException extends SQLiteException { - public SQLiteDatabaseCorruptException() {} - - public SQLiteDatabaseCorruptException(String error) { - super(error); - } -} diff --git a/src/net/sqlcipher/database/SQLiteDiskIOException.java b/src/net/sqlcipher/database/SQLiteDiskIOException.java deleted file mode 100644 index 7302abe4..00000000 --- a/src/net/sqlcipher/database/SQLiteDiskIOException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -package net.sqlcipher.database; - -/** - * An exception that indicates that an IO error occured while accessing the - * SQLite database file. - */ -public class SQLiteDiskIOException extends SQLiteException { - public SQLiteDiskIOException() {} - - public SQLiteDiskIOException(String error) { - super(error); - } -} diff --git a/src/net/sqlcipher/database/SQLiteDoneException.java b/src/net/sqlcipher/database/SQLiteDoneException.java deleted file mode 100644 index f0f6f0dc..00000000 --- a/src/net/sqlcipher/database/SQLiteDoneException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2008 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.sqlcipher.database; - -/** - * An exception that indicates that the SQLite program is done. - * Thrown when an operation that expects a row (such as {@link - * SQLiteStatement#simpleQueryForString} or {@link - * SQLiteStatement#simpleQueryForLong}) does not get one. - */ -public class SQLiteDoneException extends SQLiteException { - public SQLiteDoneException() {} - - public SQLiteDoneException(String error) { - super(error); - } -} diff --git a/src/net/sqlcipher/database/SQLiteException.java b/src/net/sqlcipher/database/SQLiteException.java deleted file mode 100644 index 2c7f11a3..00000000 --- a/src/net/sqlcipher/database/SQLiteException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -package net.sqlcipher.database; - -import net.sqlcipher.*; - -/** - * A SQLite exception that indicates there was an error with SQL parsing or execution. - */ -public class SQLiteException extends SQLException { - public SQLiteException() {} - - public SQLiteException(String error) { - super(error); - } -} diff --git a/src/net/sqlcipher/database/SQLiteFullException.java b/src/net/sqlcipher/database/SQLiteFullException.java deleted file mode 100644 index 66af19fe..00000000 --- a/src/net/sqlcipher/database/SQLiteFullException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2008 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.sqlcipher.database; - -/** - * An exception that indicates that the SQLite database is full. - */ -public class SQLiteFullException extends SQLiteException { - public SQLiteFullException() {} - - public SQLiteFullException(String error) { - super(error); - } -} diff --git a/src/net/sqlcipher/database/SQLiteMisuseException.java b/src/net/sqlcipher/database/SQLiteMisuseException.java deleted file mode 100644 index ef261fc7..00000000 --- a/src/net/sqlcipher/database/SQLiteMisuseException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2008 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.sqlcipher.database; - -public class SQLiteMisuseException extends SQLiteException { - public SQLiteMisuseException() {} - - public SQLiteMisuseException(String error) { - super(error); - } -}