Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
java-version: "17"
distribution: "adopt"

- name: Verify sqlite-android-inspection
run: ./scripts/verify-sqlite-android-inspection.sh

- name: Run Android tests
uses: reactivecircus/android-emulator-runner@v2
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
/.idea/libraries
.DS_Store
/build
sqlite-android-inspection/build/
/captures
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,34 @@ on a `SupportSQLiteOpenHelper.Configuration` and `SupportSQLiteOpenHelper.Callba
This also allows you to use sqlite-android with libraries like Room by passing an instance
of `RequerySQLiteOpenHelperFactory` to them.

### Android Studio Database Inspector (API 26+)

App Inspection’s Database Inspector expects AndroidX’s sqlite inspector and the official
`androidx.inspection` runtime (including `libart_tooling.so`). Add the optional
**`sqlite-android-inspection`** artifact **for debug builds only**:

```gradle
// Root settings.gradle: androidx.dev snapshot (see gradle.properties in this repo for build id).
dependencyResolutionManagement {
repositories {
// ... google(), mavenCentral(), etc.
maven {
url "https://androidx.dev/snapshots/builds/<BUILD_ID>/artifacts/repository"
content { includeGroup("androidx.inspection") }
}
}
}

dependencies {
implementation "com.github.requery:sqlite-android:<version>"
// JitPack multi-module artifact (see https://docs.jitpack.io/#building-a-multi-module-project)
debugImplementation "com.github.requery.sqlite-android:sqlite-android-inspection:<version>"
}
```

When consuming the library from Maven, use the same snapshot repository so the transitive
`androidx.inspection:inspection` dependency resolves. See `sqlite-android-inspection/VENDOR.txt`.


CPU Architectures
-----------------
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
plugins {
id "com.android.library" version "8.13.0" apply false
id "de.undercouch.download" version "5.6.0" apply false
id "com.google.protobuf" version "0.9.4" apply false
}
4 changes: 4 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
android.useAndroidX=true
#
# androidx.inspection snapshot (see https://androidx.dev/snapshots/builds)
androidx.inspection.snapshot.buildId=15127136
androidx.inspection.version=1.0.0-SNAPSHOT
#
GROUP=io.requery
VERSION_NAME=3.50.4-SNAPSHOT
#
Expand Down
74 changes: 74 additions & 0 deletions scripts/verify-sqlite-android-inspection.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
#
# Verifies sqlite-android-inspection: assemble, debug classpath, AAR SPI/classes.
# Usage (from repo root): ./scripts/verify-sqlite-android-inspection.sh
# Exit non-zero on any failure.

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"

echo "==> sqlite-android-inspection verification (repo: $ROOT)"
echo

echo "==> [1/5] Gradle assembleDebug"
./gradlew :sqlite-android-inspection:assembleDebug --no-daemon -q

echo "==> [2/5] Gradle assembleRelease"
./gradlew :sqlite-android-inspection:assembleRelease --no-daemon -q

echo "==> [3/5] debugCompileClasspath includes androidx.inspection:inspection"
if ! ./gradlew :sqlite-android-inspection:dependencies --configuration debugCompileClasspath --no-daemon -q 2>/dev/null | grep -q 'androidx.inspection:inspection'; then
echo "ERROR: androidx.inspection:inspection not found on debugCompileClasspath (check snapshot repo / androidx.inspection.snapshot.buildId)" >&2
exit 1
fi

AAR_DIR="$ROOT/sqlite-android-inspection/build/outputs/aar"
AAR=""
if [[ -d "$AAR_DIR" ]]; then
AAR=$(ls -1 "$AAR_DIR"/*-debug.aar 2>/dev/null | head -1 || true)
fi

if [[ -z "${AAR:-}" || ! -f "$AAR" ]]; then
echo "ERROR: expected debug AAR under $AAR_DIR" >&2
exit 1
fi
echo "==> [4/5] AAR: $AAR"

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

unzip -q -o "$AAR" -d "$TMP"

JAR="$TMP/classes.jar"
if [[ ! -f "$JAR" ]]; then
echo "ERROR: classes.jar missing in AAR" >&2
exit 1
fi

SPI_REL="META-INF/services/androidx.inspection.InspectorFactory"
if ! jar tf "$JAR" | grep -q "^${SPI_REL}\$"; then
echo "ERROR: missing $SPI_REL in classes.jar" >&2
exit 1
fi
SPI_CONTENT="$(unzip -p "$JAR" "$SPI_REL")"
if ! grep -q 'androidx\.sqlite\.inspection\.SqliteInspectorFactory' <<<"$SPI_CONTENT"; then
echo "ERROR: SPI factory line not found in $SPI_REL" >&2
printf '%s\n' "$SPI_CONTENT" >&2
exit 1
fi
echo " SPI OK: $(echo "$SPI_CONTENT" | tr -d '\n')"

for class in \
'androidx/sqlite/inspection/SqliteInspector.class' \
'androidx/sqlite/inspection/SqliteInspectorFactory.class'; do
if ! jar tf "$JAR" | grep -q "^$class\$"; then
echo "ERROR: missing $class in classes.jar" >&2
exit 1
fi
echo " class OK: $class"
done

echo
echo "==> [5/5] All checks passed. E2E: :sqlite-android-inspection:connectedDebugAndroidTest (see VENDOR.txt)."
10 changes: 10 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
def props = new Properties()
file("${rootDir}/gradle.properties").withInputStream { props.load(it) }
def buildId = props.getProperty("androidx.inspection.snapshot.buildId", "15127136")
maven {
url = uri("https://androidx.dev/snapshots/builds/${buildId}/artifacts/repository")
content {
includeGroup("androidx.inspection")
}
}
}
}

rootProject.name = 'requery-sqlite'

include ':sqlite-android'
include ':sqlite-android-inspection'
16 changes: 16 additions & 0 deletions sqlite-android-inspection/VENDOR.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
androidx.sqlite.inspection fork for io.requery (OpenParams-free), plus proto and SPI
(META-INF/services/androidx.inspection.InspectorFactory → SqliteInspectorFactory).

Runtime androidx.inspection.* and libart_tooling.so come from Maven artifact
androidx.inspection:inspection (not vendored here). Snapshot repo and build id:
gradle.properties (androidx.inspection.version, androidx.inspection.snapshot.buildId);
settings.gradle adds https://androidx.dev/snapshots/builds/<BUILD_ID>/artifacts/repository

debugApi(inspection): transitive for debug consumers. releaseCompileOnly: release AAR does not bundle JVMTI.

App: debugImplementation(project(":sqlite-android-inspection")) or published coordinates.
If the app resolves this module from Maven (not project()), add the same androidx.dev snapshot repo
(or a mirror) so transitive androidx.inspection:inspection resolves.

Checks: ./scripts/verify-sqlite-android-inspection.sh
E2E (device): ./gradlew :sqlite-android-inspection:connectedDebugAndroidTest (InspectionJvmtiRuntimeTest)
59 changes: 59 additions & 0 deletions sqlite-android-inspection/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
plugins {
id "com.android.library"
id "com.google.protobuf"
id "com.vanniktech.maven.publish" version "0.34.0"
}

android {
buildToolsVersion = "36.1.0"
compileSdk {
version = release(36)
}
namespace "androidx.sqlite.inspection"
defaultConfig {
minSdkVersion 26
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
lint {
abortOnError false
}
}

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.3"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}

dependencies {
def inspVersion = findProperty("androidx.inspection.version") ?: "1.0.0-SNAPSHOT"
debugApi("androidx.inspection:inspection:${inspVersion}")
releaseCompileOnly("androidx.inspection:inspection:${inspVersion}")

compileOnly("androidx.annotation:annotation:1.8.1")
api("org.jspecify:jspecify:1.0.0")
api("com.google.protobuf:protobuf-javalite:3.25.3")
api(project(":sqlite-android"))

androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
}

mavenPublishing {
publishToMavenCentral(/* automaticRelease */ true)
signAllPublications()
}
11 changes: 11 additions & 0 deletions sqlite-android-inspection/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# App Inspection / Database Inspector (vendored androidx sqlite-inspection + io.requery hooks)
-keep class androidx.sqlite.inspection.** { *; }
-keep class androidx.inspection.** { *; }
-keepclassmembers class io.requery.android.database.sqlite.SQLiteDatabase {
public static *** openDatabase(...);
public *** rawQueryWithFactory(...);
void endTransaction();
}
-keep class io.requery.android.database.sqlite.SQLiteClosable { *; }
-keep class io.requery.android.database.sqlite.SQLiteCursor { *; }
-keep class io.requery.android.database.sqlite.SQLiteStatement { *; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package androidx.sqlite.inspection;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import android.os.Build;
import androidx.inspection.ArtToolingImpl;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Runtime check: inspection AAR supplies libart_tooling.so in APK, loads, {@link ArtToolingImpl} initializes. */
@RunWith(AndroidJUnit4.class)
public class InspectionJvmtiRuntimeTest {

@Test
public void libArtTooling_so_inBaseApk_forPrimaryAbi() throws Exception {
String apkPath =
InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageCodePath();
String abi = Build.SUPPORTED_ABIS[0];
String entryName = "lib/" + abi + "/libart_tooling.so";
try (ZipFile zf = new ZipFile(apkPath)) {
ZipEntry entry = zf.getEntry(entryName);
assertTrue(
"Missing " + entryName + " in " + apkPath + " (add androidx.inspection:inspection)",
entry != null);
}
}

@Test
public void art_tooling_nativeLibrary_loads() {
System.loadLibrary("art_tooling");
}

@Test
public void artToolingImpl_instance_initializes() {
assertNotNull(ArtToolingImpl.instance());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2020 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 androidx.sqlite.inspection;

import io.requery.android.database.sqlite.SQLiteDatabase;

import org.jspecify.annotations.NonNull;

import java.io.File;
import java.util.Objects;

final class DatabaseExtensions {
private static final String sInMemoryDatabasePath = ":memory:";

/** Placeholder {@code %x} is for database's hashcode */
private static final String sInMemoryDatabaseNameFormat =
sInMemoryDatabasePath + " {hashcode=0x%x}";

private DatabaseExtensions() { }

/** Thread-safe as {@link SQLiteDatabase#getPath} and {@link Object#hashCode) are thread-safe.*/
static String pathForDatabase(@NonNull SQLiteDatabase database) {
return isInMemoryDatabase(database)
? String.format(sInMemoryDatabaseNameFormat, database.hashCode())
: new File(database.getPath()).getAbsolutePath();
}

/** Thread-safe as {@link SQLiteDatabase#getPath} is thread-safe. */
static boolean isInMemoryDatabase(@NonNull SQLiteDatabase database) {
return Objects.equals(sInMemoryDatabasePath, database.getPath());
}

/**
* Attempts to call {@link SQLiteDatabase#acquireReference} on the provided object.
*
* @return true if the operation was successful; false if unsuccessful because the database
* was already closed; otherwise re-throws the exception thrown by
* {@link SQLiteDatabase#acquireReference}.
*/
static boolean tryAcquireReference(@NonNull SQLiteDatabase database) {
if (!database.isOpen()) {
return false;
}

try {
database.acquireReference();
return true; // success
} catch (IllegalStateException e) {
if (isAttemptAtUsingClosedDatabase(e)) {
return false;
}
throw e;
}
}

/**
* Note that this is best-effort as relies on Exception message parsing, which could break in
* the future.
* Use in the context where false negatives (more likely) and false positives (less likely
* due to the specificity of the message) are tolerable, e.g. to assign error codes where if
* it fails we will just send an 'unknown' error.
*/
static boolean isAttemptAtUsingClosedDatabase(IllegalStateException exception) {
String message = exception.getMessage();
return message != null && (message.contains("attempt to re-open an already-closed object")
|| message.contains(
"Cannot perform this operation because the connection pool has been closed"));
}
}
Loading