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