diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6d691f59d..c98ef91cb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -45,6 +45,24 @@ jobs:
- name: spotless:check
run: mvn --batch-mode --no-transfer-progress spotless:check
+ android-signatures:
+ name: Check Android signatures
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin'
+ java-version: 11
+ java-package: jdk
+ cache: 'maven'
+ - name: Download and install signatures jar
+ run: |
+ wget -O coreLib2.signature https://repo1.maven.org/maven2/com/toasttab/android/gummy-bears-api-24/0.12.0/gummy-bears-api-24-0.12.0-coreLib2.signature
+ mvn --batch-mode --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.1:install-file -Dfile=./coreLib2.signature -DgroupId=org.sqlite.signatures -DartifactId=gummy-bears-api-24 -Dversion=0.1 -Dpackaging=signature
+ - name: animal-sniffer:check
+ run: mvn --batch-mode --no-transfer-progress compile animal-sniffer:check
+
test:
name: test ${{ matrix.os }} jdk${{ matrix.java }}
strategy:
@@ -190,7 +208,7 @@ jobs:
release:
name: Deploy
- needs: [ lint, test, test_multiarch, test_external_amalgamation, test_graalvm ]
+ needs: [ lint, android-signatures, test, test_multiarch, test_external_amalgamation, test_graalvm ]
if: github.repository_owner == 'xerial' && github.ref == 'refs/heads/master' # only perform on latest master
runs-on: ubuntu-latest
steps:
diff --git a/USAGE.md b/USAGE.md
index 258762d18..06e4c9c5a 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -202,6 +202,11 @@ The name of directories in our jar and in Android Studio differ, here is a mappi
| x86 | x86 |
| x86_64 | x86_64 |
+Your project will need to integrate the [desugared core library](https://developer.android.com/studio/write/java11-default-support-table) (default).
+
+The following methods will not work in Android:
+- `JDBC3PreparedStatement#getParameterTypeName`
+
## How to load Run-Time Loadable Extensions
### Enable loadable extensions
diff --git a/pom.xml b/pom.xml
index faac1ff91..d3f59f3b6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -234,6 +234,31 @@
+
+
+ org.codehaus.mojo
+ animal-sniffer-maven-plugin
+ 1.27
+
+
+
+
+
+
+
+
+
+
+
+ org.sqlite.signatures
+ gummy-bears-api-24
+ 0.1
+
+
+ org.sqlite.util.AndroidSignatureIgnore
+
+
+
diff --git a/src/main/java/org/sqlite/SQLiteConnection.java b/src/main/java/org/sqlite/SQLiteConnection.java
index 9837bedf1..6fcb8f018 100644
--- a/src/main/java/org/sqlite/SQLiteConnection.java
+++ b/src/main/java/org/sqlite/SQLiteConnection.java
@@ -1,14 +1,14 @@
package org.sqlite;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
-import java.nio.file.Files;
-import java.nio.file.StandardCopyOption;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
@@ -316,8 +316,16 @@ private static File extractResource(URL resourceAddr) throws IOException {
URLConnection conn = resourceAddr.openConnection();
// Disable caches to avoid keeping unnecessary file references after the single-use copy
conn.setUseCaches(false);
- try (InputStream reader = conn.getInputStream()) {
- Files.copy(reader, dbFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ try (InputStream reader = conn.getInputStream();
+ OutputStream writer = new FileOutputStream(dbFile)) {
+ // Replace this code with buffer copy so we don't rely on java.nio package for Android
+ // compatibility
+ // Files.copy(reader, dbFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, bytesRead);
+ }
return dbFile;
}
}
diff --git a/src/main/java/org/sqlite/SQLiteJDBCLoader.java b/src/main/java/org/sqlite/SQLiteJDBCLoader.java
index cfd41abc8..f430dccac 100644
--- a/src/main/java/org/sqlite/SQLiteJDBCLoader.java
+++ b/src/main/java/org/sqlite/SQLiteJDBCLoader.java
@@ -40,6 +40,7 @@
import java.util.Properties;
import java.util.UUID;
import java.util.stream.Stream;
+import org.sqlite.util.AndroidSignatureIgnore;
import org.sqlite.util.LibraryLoaderUtil;
import org.sqlite.util.Logger;
import org.sqlite.util.LoggerFactory;
@@ -57,6 +58,7 @@
*
* @author leo
*/
+@AndroidSignatureIgnore(explanation = "The loader is not used on Android")
public class SQLiteJDBCLoader {
private static final Logger logger = LoggerFactory.getLogger(SQLiteJDBCLoader.class);
diff --git a/src/main/java/org/sqlite/core/DB.java b/src/main/java/org/sqlite/core/DB.java
index bd204c34f..efd7dc28f 100644
--- a/src/main/java/org/sqlite/core/DB.java
+++ b/src/main/java/org/sqlite/core/DB.java
@@ -17,6 +17,7 @@
import java.sql.BatchUpdateException;
import java.sql.SQLException;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -941,11 +942,13 @@ private synchronized long[] executeBatch(
if (rc != SQLITE_DONE) {
reset(stmt);
if (rc == SQLITE_ROW) {
+ // don't use the constructor with long because of
+ // https://github.com/xerial/sqlite-jdbc/issues/1378
throw new BatchUpdateException(
"batch entry " + i + ": query returns results",
null,
0,
- changes,
+ Arrays.stream(changes).mapToInt(l -> (int) l).toArray(),
null);
}
throwex(rc);
diff --git a/src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java b/src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java
index 542f0b95b..3b8a08863 100644
--- a/src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java
+++ b/src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java
@@ -25,6 +25,7 @@
import org.sqlite.SQLiteConnection;
import org.sqlite.core.CorePreparedStatement;
import org.sqlite.core.DB;
+import org.sqlite.util.AndroidSignatureIgnore;
public abstract class JDBC3PreparedStatement extends CorePreparedStatement {
@@ -169,6 +170,7 @@ public String getParameterClassName(int param) throws SQLException {
}
/** @see java.sql.ParameterMetaData#getParameterTypeName(int) */
+ @AndroidSignatureIgnore(explanation = "Android does not support java.sql.JDBCType")
public String getParameterTypeName(int pos) throws SQLException {
checkIndex(pos);
return JDBCType.valueOf(getParameterType(pos)).getName();
diff --git a/src/main/java/org/sqlite/jdbc3/JDBC3Statement.java b/src/main/java/org/sqlite/jdbc3/JDBC3Statement.java
index 1af5694bd..5a1c4d681 100644
--- a/src/main/java/org/sqlite/jdbc3/JDBC3Statement.java
+++ b/src/main/java/org/sqlite/jdbc3/JDBC3Statement.java
@@ -245,8 +245,14 @@ public long[] executeLargeBatch() throws SQLException {
db.prepare(this);
changes[i] = db.executeUpdate(this, null);
} catch (SQLException e) {
+ // don't use the constructor with long because of
+ // https://github.com/xerial/sqlite-jdbc/issues/1378
throw new BatchUpdateException(
- "batch entry " + i + ": " + e.getMessage(), null, 0, changes, e);
+ "batch entry " + i + ": " + e.getMessage(),
+ null,
+ 0,
+ Arrays.stream(changes).mapToInt(l -> (int) l).toArray(),
+ e);
} finally {
if (pointer != null) pointer.close();
}
diff --git a/src/main/java/org/sqlite/jdbc4/JDBC4ResultSet.java b/src/main/java/org/sqlite/jdbc4/JDBC4ResultSet.java
index 9747b0d68..146fe25e3 100644
--- a/src/main/java/org/sqlite/jdbc4/JDBC4ResultSet.java
+++ b/src/main/java/org/sqlite/jdbc4/JDBC4ResultSet.java
@@ -325,7 +325,11 @@ public T getObject(int columnIndex, Class type) throws SQLException {
if (type == LocalDate.class) {
try {
Date date = getDate(columnIndex);
- if (date != null) return type.cast(date.toLocalDate());
+ if (date != null)
+ // inlining of java.sql.Date.toLocateDate() for Android
+ return type.cast(
+ LocalDate.of(
+ date.getYear() + 1900, date.getMonth() + 1, date.getDate()));
else return null;
} catch (SQLException sqlException) {
// If the FastDateParser failed, try parse it with LocalDate.
@@ -336,7 +340,10 @@ public T getObject(int columnIndex, Class type) throws SQLException {
if (type == LocalTime.class) {
try {
Time time = getTime(columnIndex);
- if (time != null) return type.cast(time.toLocalTime());
+ if (time != null)
+ // inlining of java.sql.Date.toLocateTime() for Android
+ return type.cast(
+ LocalTime.of(time.getHours(), time.getMinutes(), time.getSeconds()));
else return null;
} catch (SQLException sqlException) {
// If the FastDateParser failed, try parse it with LocalTime.
@@ -347,7 +354,17 @@ public T getObject(int columnIndex, Class type) throws SQLException {
if (type == LocalDateTime.class) {
try {
Timestamp timestamp = getTimestamp(columnIndex);
- if (timestamp != null) return type.cast(timestamp.toLocalDateTime());
+ if (timestamp != null)
+ // inlining of java.sql.Date.toLocateDateTime() for Android
+ return type.cast(
+ LocalDateTime.of(
+ timestamp.getYear() + 1900,
+ timestamp.getMonth() + 1,
+ timestamp.getDate(),
+ timestamp.getHours(),
+ timestamp.getMinutes(),
+ timestamp.getSeconds(),
+ timestamp.getNanos()));
else return null;
} catch (SQLException e) {
// If the FastDateParser failed, try parse it with LocalDateTime.
diff --git a/src/main/java/org/sqlite/util/AndroidSignatureIgnore.java b/src/main/java/org/sqlite/util/AndroidSignatureIgnore.java
new file mode 100644
index 000000000..846ce0765
--- /dev/null
+++ b/src/main/java/org/sqlite/util/AndroidSignatureIgnore.java
@@ -0,0 +1,14 @@
+package org.sqlite.util;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE, ElementType.FIELD})
+public @interface AndroidSignatureIgnore {
+ String explanation();
+}
diff --git a/src/main/java/org/sqlite/util/ProcessRunner.java b/src/main/java/org/sqlite/util/ProcessRunner.java
index 977a80546..bea35fa8d 100644
--- a/src/main/java/org/sqlite/util/ProcessRunner.java
+++ b/src/main/java/org/sqlite/util/ProcessRunner.java
@@ -3,7 +3,6 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.util.concurrent.TimeUnit;
public class ProcessRunner {
String runAndWaitFor(String command) throws IOException, InterruptedException {
@@ -13,14 +12,6 @@ String runAndWaitFor(String command) throws IOException, InterruptedException {
return getProcessOutput(p);
}
- String runAndWaitFor(String command, long timeout, TimeUnit unit)
- throws IOException, InterruptedException {
- Process p = Runtime.getRuntime().exec(command);
- p.waitFor(timeout, unit);
-
- return getProcessOutput(p);
- }
-
static String getProcessOutput(Process process) throws IOException {
try (InputStream in = process.getInputStream()) {
int readLen;
diff --git a/src/main/java9/org/sqlite/nativeimage/SqliteJdbcFeature.java b/src/main/java9/org/sqlite/nativeimage/SqliteJdbcFeature.java
index 17082155c..e6b278b14 100644
--- a/src/main/java9/org/sqlite/nativeimage/SqliteJdbcFeature.java
+++ b/src/main/java9/org/sqlite/nativeimage/SqliteJdbcFeature.java
@@ -4,10 +4,15 @@
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
import org.graalvm.nativeimage.hosted.RuntimeJNIAccess;
import org.graalvm.nativeimage.hosted.RuntimeResourceAccess;
-import org.sqlite.*;
+import org.sqlite.BusyHandler;
+import org.sqlite.Collation;
+import org.sqlite.Function;
+import org.sqlite.ProgressHandler;
+import org.sqlite.SQLiteJDBCLoader;
import org.sqlite.core.DB;
import org.sqlite.core.NativeDB;
import org.sqlite.jdbc3.JDBC3DatabaseMetaData;
+import org.sqlite.util.AndroidSignatureIgnore;
import org.sqlite.util.LibraryLoaderUtil;
import org.sqlite.util.OSInfo;
import org.sqlite.util.ProcessRunner;
@@ -21,6 +26,7 @@
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
+@AndroidSignatureIgnore(explanation = "Used by GraalVM only")
public class SqliteJdbcFeature implements Feature {
@Override