diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 14f1f078..ebbfb89e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,49 +1,98 @@
---
name: Bug report
-about: Create a report to help us improve
+about: You found a bug in ObjectBox causing an application to crash or throw an exception, or something does not work right.
title: ''
labels: 'bug'
assignees: ''
---
-:rotating_light: First, please check:
- - existing issues,
- - Docs https://docs.objectbox.io/
- - Troubleshooting page https://docs.objectbox.io/troubleshooting
- - FAQ page https://docs.objectbox.io/faq
-
-**Describe the bug**
-A clear and concise description in English of what the bug is.
-
-**Basic info (please complete the following information):**
- - ObjectBox version (are you using the latest version?): [e.g. 2.7.0]
- - Reproducibility: [e.g. occurred once only | occasionally without visible pattern | always]
- - Device: [e.g. Galaxy S20]
- - OS: [e.g. Android 10]
-
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Put '...'
-2. Make changes to '....'
-3. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Code**
-If applicable, add code to help explain your problem.
- - Include affected entity classes.
- - Please remove any unnecessary or confidential parts.
- - At best, link to or attach a project with a failing test.
-
-**Logs, stack traces**
-If applicable, add relevant logs, or a stack trace.
- - For __build issues__, use `--stacktrace` for the Gradle build (`./gradlew build --stacktrace`).
- - For __runtime errors__, check Android's Logcat (also check logs before the issue!).
-
-**Additional context**
-Add any other context about the problem here.
- - Is there anything special about your app?
- - May transactions or multi-threading play a role?
- - Did you find any workarounds to prevent the issue?
+
+
+### Is there an existing issue?
+
+- [ ] I have searched [existing issues](https://github.com/objectbox/objectbox-java/issues)
+
+### Build info
+
+- ObjectBox version: [e.g. 3.7.0]
+- OS: [e.g. Android 14 | Ubuntu 22.04 | Windows 11 ]
+- Device/ABI/architecture: [e.g. Galaxy S23 | arm64-v8a | x86-64 ]
+
+### Steps to reproduce
+
+_TODO Tell us exactly how to reproduce the problem._
+
+1. ...
+2. ...
+3. ...
+
+### Expected behavior
+
+_TODO Tell us what you expect to happen._
+
+### Actual behavior
+
+_TODO Tell us what actually happens._
+
+
+### Code
+
+_TODO Add a code example to help us reproduce your problem._
+
+
+
+Code
+
+```java
+[Paste your code here]
+```
+
+
+
+### Logs, stack traces
+
+_TODO Add relevant logs, a stack trace or crash report._
+
+
+
+Logs
+
+```console
+[Paste your logs here]
+```
+
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 975b320b..1846a02e 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,26 +1,37 @@
---
name: Feature request
-about: Suggest an idea
+about: Suggest an improvement for ObjectBox.
title: ''
-labels: 'feature'
+labels: 'enhancement'
assignees: ''
---
-:rotating_light: First, please check:
- - existing issues,
- - Docs https://docs.objectbox.io/
- - Troubleshooting page https://docs.objectbox.io/troubleshooting
- - FAQ page https://docs.objectbox.io/faq
+
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
+### Is there an existing issue?
-**Additional context**
-Add any other context (e.g. platform or language) about the feature request here.
+- [ ] I have searched [existing issues](https://github.com/objectbox/objectbox-java/issues)
+
+### Use case
+
+_TODO Describe what problem you are trying to solve._
+
+### Proposed solution
+
+_TODO Describe what you want to be able to do with ObjectBox._
+
+### Alternatives
+
+_TODO Describe any alternative solutions or features you've considered._
+
+### Additional context
+
+_TODO Add any other context (e.g. platform or language) about the feature request here._
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..2c431b0b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/close-no-response.yml b/.github/workflows/close-no-response.yml
index 9500c6e7..3e906671 100644
--- a/.github/workflows/close-no-response.yml
+++ b/.github/workflows/close-no-response.yml
@@ -2,6 +2,11 @@ name: Close inactive issues
on:
schedule:
- cron: "15 1 * * *" # “At 01:15.”
+ workflow_dispatch: # To support running manually.
+
+# Minimal access by default
+permissions:
+ contents: read
jobs:
close-issues:
@@ -11,7 +16,7 @@ jobs:
pull-requests: write
steps:
# https://github.com/marketplace/actions/close-stale-issues
- - uses: actions/stale@v5
+ - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
days-before-stale: -1 # Add the stale label manually.
days-before-close: 21
diff --git a/.gitignore b/.gitignore
index 5f02f8d1..1bedca93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,9 @@ gen/
target/
out/
classes/
-gradle.properties
+
+# Kotlin
+.kotlin
# Local build properties
build.properties
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 58b31c18..63a5ea1d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,165 +1,238 @@
-# Default image for linux builds
-image: objectboxio/buildenv:21.11.11-centos7
+# https://docs.gitlab.com/ci/yaml/
# Assumes these environment variables are configured in GitLab CI/CD Settings:
+# - OBX_READ_PACKAGES_TOKEN
# - SONATYPE_USER
# - SONATYPE_PWD
# - GOOGLE_CHAT_WEBHOOK_JAVA_CI
-# Additionally, Gradle scripts assume these Gradle project properties are set:
-# https://docs.gradle.org/current/userguide/build_environment.html#sec:project_properties
+# Additionally, these environment variables used by the objectbox-publish Gradle script:
# - ORG_GRADLE_PROJECT_signingKeyFile
# - ORG_GRADLE_PROJECT_signingKeyId
# - ORG_GRADLE_PROJECT_signingPassword
variables:
+ OBX_PUBLISH_RELEASE:
+ value: "false"
+ options: [ "false", "true" ]
+ description: "If enabled, builds and depends on release versions and triggers publishing a
+ release of the Java library artifacts to the internal GitLab repository and Maven Central.
+ Don't run multiple times to avoid duplicate artifacts in the GitLab repository.
+ Consult the release checklist before turning this on."
+ # If enabled, builds and depends on release versions. Doesn't publish a release.
+ # See the root Gradle build script for details.
+ OBX_RELEASE: "false"
+
# Disable the Gradle daemon. Gradle may run in a Docker container with a shared
# Docker volume containing GRADLE_USER_HOME. If the container is stopped after a job
# Gradle daemons may get killed, preventing proper clean-up of lock files in GRADLE_USER_HOME.
- # Configure file.encoding to always use UTF-8 when running Gradle.
# Use low priority processes to avoid Gradle builds consuming all build machine resources.
- GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dfile.encoding=UTF-8 -Dorg.gradle.priority=low"
- GITLAB_REPO_ARGS: "-PgitlabUrl=$CI_SERVER_URL -PgitlabTokenName=Job-Token -PgitlabPrivateToken=$CI_JOB_TOKEN"
- CENTRAL_REPO_ARGS: "-PsonatypeUsername=$SONATYPE_USER -PsonatypePassword=$SONATYPE_PWD"
+ GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.priority=low"
+ GITLAB_REPO_ARGS: "-PgitlabUrl=$CI_SERVER_URL -PgitlabPrivateTokenName=Deploy-Token -PgitlabPrivateToken=$OBX_READ_PACKAGES_TOKEN"
+ GITLAB_PUBLISH_ARGS: "-PgitlabPublishTokenName=Job-Token -PgitlabPublishToken=$CI_JOB_TOKEN"
+ CENTRAL_PUBLISH_ARGS: "-PsonatypeUsername=$SONATYPE_USER -PsonatypePassword=$SONATYPE_PWD"
# CI_COMMIT_REF_SLUG is the branch or tag name, but web-safe (only 0-9, a-z)
- VERSION_ARGS: "-PversionPostFix=$CI_COMMIT_REF_SLUG"
+ VERSION_ARGS: "-PversionSuffix=$CI_COMMIT_REF_SLUG"
# Using multiple test stages to avoid running some things in parallel (see job notes).
stages:
- test
- - upload-to-internal
- - upload-to-central
+ - publish-maven-internal
+ - publish-maven-central
- package-api-docs
- triggers
-test:
- stage: test
- tags: [ docker, linux, x64 ]
+workflow:
+ rules:
+ # Disable merge request pipelines https://docs.gitlab.com/ci/jobs/job_rules/#ci_pipeline_source-predefined-variable
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+ when: never
+ # On main branch, always use release versions as old snapshots of dependencies are deleted
+ - if: $CI_COMMIT_REF_NAME == "main"
+ variables:
+ OBX_RELEASE: "true"
+ # To publish a release must use release versions
+ - if: $OBX_PUBLISH_RELEASE == "true"
+ variables:
+ OBX_RELEASE: "true"
+ # In all other cases, create a pipeline
+ - when: always
+
+.docker_linux_x64_android: &docker_linux_x64_android
+ tags:
+ - docker
+ - linux
+ - x64
+ image: objectboxio/buildenv-android:2026-05-12 # With JDK 21, Android build-tools;35.0.0 and platforms;android-35 pre-installed
variables:
- # CentOS 7 defaults to ASCII, use a UTF-8 compatible locale so UTF-8 tests that interact with file system work.
- LC_ALL: "en_US.UTF-8"
+ # Image defaults to POSIX (ASCII), set a compatible locale so UTF-8 tests that interact with the file system work.
+ # Check with 'locale -a' for available locales.
+ LC_ALL: "C.UTF-8"
+
+.test_base:
before_script:
# Print Gradle and JVM version info
- ./gradlew -version
# Remove any previous JVM (Hotspot) crash log.
# "|| true" for an OK exit code if no file is found
- rm **/hs_err_pid*.log || true
- script:
- - ./ci/test-with-asan.sh $GITLAB_REPO_ARGS $VERSION_ARGS clean build
+ # By default, don't compile and test the Android APIs (only do in test job), there is currently no benefit to test
+ # them using multiple platforms or JDKs.
+ script: ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS clean build -PexcludeAndroid=true
artifacts:
when: always
paths:
- - "**/hs_err_pid*.log" # Only on JVM (Hotspot) crash.
- - "**/build/reports/spotbugs/*.html"
+ - "**/hs_err_pid*.log" # Only on JVM (Hotspot) crash
reports:
junit: "**/build/test-results/**/TEST-*.xml"
-.test-template:
- before_script:
- # Print Gradle and JVM version info
- - ./gradlew -version
- # Remove any previous JVM (Hotspot) crash log.
- # "|| true" for an OK exit code if no file is found
- - rm **/hs_err_pid*.log || true
+test:
+ <<: *docker_linux_x64_android
+ extends: .test_base
+ stage: test
+ # Overwrite script key to run through ASAN script, compile and test Android APIs and test build of Web API docs.
+ # Note: Address sanitizer is only available on Linux runners (see script).
+ script:
+ # build to assemble, run tests and spotbugs
+ # javadocForWeb to catch API docs errors before releasing
+ # Temporarily disable testing with Address Sanitizer until buildenv images are modernized, see #273
+ # - ./scripts/test-with-asan.sh $GITLAB_REPO_ARGS $VERSION_ARGS clean build javadocForWeb
+ - ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS clean build javadocForWeb
+ # Overwrite artifacts key to archive spotbugs reports
artifacts:
when: always
paths:
- "**/hs_err_pid*.log" # Only on JVM (Hotspot) crash.
+ - "**/build/reports/spotbugs/*.html"
reports:
junit: "**/build/test-results/**/TEST-*.xml"
-test-windows:
- extends: .test-template
- needs: ["test"]
- tags: [ windows ]
- script: ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS clean build
-
-test-macos:
- extends: .test-template
- needs: ["test"]
- tags: [mac11+, x64]
- script: ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS clean build
-
-# Address sanitizer is only available on Linux runners (see script).
-.test-asan-template:
- extends: .test-template
- tags: [ docker, linux, x64 ]
- variables:
- # CentOS 7 defaults to ASCII, use a UTF-8 compatible locale so UTF-8 tests that interact with file system work.
- LC_ALL: "en_US.UTF-8"
+.test_jdks:
+ <<: *docker_linux_x64_android
+ extends: .test_base
+ # Overwrite script key to run through ASAN script and to run test task directly to avoid
+ # running SpotBugs (build runs check runs SpotBugs tasks).
+ # Note: Address sanitizer is only available on Linux runners (see script).
script:
- # Note: do not run check task as it includes SpotBugs.
- - ./ci/test-with-asan.sh $GITLAB_REPO_ARGS $VERSION_ARGS clean :tests:objectbox-java-test:test
+ # Temporarily disable testing with Address Sanitizer until buildenv images are modernized, see #273
+ # - ./scripts/test-with-asan.sh $GITLAB_REPO_ARGS $VERSION_ARGS clean :tests:objectbox-java-test:test
+ - ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS clean :tests:objectbox-java-test:test
# Test oldest supported and a recent JDK.
-# Note: can not run these in parallel using a matrix configuration as Gradle would step over itself.
test-jdk-8:
- extends: .test-asan-template
+ extends: .test_jdks
needs: ["test"]
variables:
TEST_JDK: 8
-# JDK 17 is the latest LTS release.
-test-jdk-17:
- extends: .test-asan-template
- needs: ["test-jdk-8"]
+# JDK 21 is the default of the current build image, so test the latest LTS JDK 25
+test-jdk-25:
+ extends: .test_jdks
+ needs: ["test"]
variables:
- TEST_JDK: 17
+ TEST_JDK: 25
+
+test-macos:
+ extends: .test_base
+ tags:
+ - jdk
+ - mac
+
+test-windows:
+ extends: .test_base
+ tags:
+ - jdk
+ - windows
+ - x64
-test-jdk-x86:
- extends: .test-template
- needs: ["test-windows"]
- tags: [ windows ]
+test-windows-jdk-x86:
+ extends: .test_base
+ tags:
+ - jdk-x86
+ - windows
variables:
# TEST_WITH_JAVA_X86 makes objectbox-java-test use 32-bit java executable and therefore
# 32-bit ObjectBox to run tests (see build.gradle file).
# Note: assumes JAVA_HOME_X86 is set to 32-bit JDK path.
TEST_WITH_JAVA_X86: "true"
- script: ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS clean build
-upload-to-internal:
- stage: upload-to-internal
- tags: [ docker, x64 ]
- except:
- - tags # Only publish from branches.
+# Publish Maven artifacts to internal Maven repo
+publish-maven-internal:
+ <<: *docker_linux_x64_android
+ stage: publish-maven-internal
+ rules:
+ # Not for tag pipelines (Maven artifact snapshots are versioned using the branch name)
+ - if: $CI_COMMIT_TAG
+ when: never
+ # Not when using release versions and not publishing a release to avoid duplicate artifacts
+ # (GitLab would allow to upload duplicates for a release version)
+ - if: $OBX_RELEASE == "true" && $OBX_PUBLISH_RELEASE != "true"
+ when: never
+ # Not if triggered by upstream project to save on disk space
+ - if: $CI_PIPELINE_SOURCE == "pipeline"
+ when: never
+ # Not for scheduled pipelines to save on disk space
+ - if: $CI_PIPELINE_SOURCE == "schedule"
+ when: never
+ # Otherwise, only if no previous stages failed
+ - when: on_success
script:
- - ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS publishMavenJavaPublicationToGitLabRepository
+ - ./gradlew $GITLAB_REPO_ARGS $GITLAB_PUBLISH_ARGS $VERSION_ARGS publishAllPublicationsToGitLabRepository
-upload-to-central:
- stage: upload-to-central
- tags: [ docker, x64 ]
- only:
- - publish
+# Publish Maven artifacts to public Maven Central repo
+publish-maven-central:
+ <<: *docker_linux_x64_android
+ stage: publish-maven-central
+ rules:
+ # Only if publishing a release, only if no previous stages failed
+ - if: $OBX_PUBLISH_RELEASE == "true"
+ when: on_success
before_script:
- - ci/send-to-gchat.sh "$GOOGLE_CHAT_WEBHOOK_JAVA_CI" --thread $CI_COMMIT_SHA "*Releasing Java library:* job $CI_JOB_NAME from branch $CI_COMMIT_BRANCH ($CI_COMMIT_SHORT_SHA)..."
+ - ci/send-to-gchat.sh "$GOOGLE_CHAT_WEBHOOK_JAVA_CI" --thread $CI_COMMIT_SHA "*Releasing Java libraries (job = $CI_JOB_NAME, ref = $CI_COMMIT_REF_NAME, commit = $CI_COMMIT_SHORT_SHA) ..."
script:
- # Note: supply internal repo as tests use native dependencies that might not be published, yet.
- - ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS $CENTRAL_REPO_ARGS publishMavenJavaPublicationToSonatypeRepository closeAndReleaseSonatypeStagingRepository
+ # Note: supply internal repo as tests use native dependencies that might not be available publicly, yet.
+ - ./gradlew $GITLAB_REPO_ARGS $CENTRAL_PUBLISH_ARGS publishAllPublicationsToSonatypeRepository closeAndReleaseSonatypeStagingRepository
after_script:
- # Also runs on failure, so show CI_JOB_STATUS.
- - ci/send-to-gchat.sh "$GOOGLE_CHAT_WEBHOOK_JAVA_CI" --thread $CI_COMMIT_SHA "*Releasing Java library:* *$CI_JOB_STATUS* for $CI_JOB_NAME"
+ # Also runs on failure, so print CI_JOB_STATUS
+ - ci/send-to-gchat.sh "$GOOGLE_CHAT_WEBHOOK_JAVA_CI" --thread $CI_COMMIT_SHA "*Releasing Java libraries (job = $CI_JOB_NAME, ref = $CI_COMMIT_REF_NAME, commit = $CI_COMMIT_SHORT_SHA) job status = *$CI_JOB_STATUS*"
- ci/send-to-gchat.sh "$GOOGLE_CHAT_WEBHOOK_JAVA_CI" --thread $CI_COMMIT_SHA "Check https://repo1.maven.org/maven2/io/objectbox/ in a few minutes."
+# Create Java API docs archive
package-api-docs:
+ <<: *docker_linux_x64_android
stage: package-api-docs
- tags: [ docker, x64 ]
- only:
- - publish
+ rules:
+ # Only if publishing a release, only if no previous stages failed
+ - if: $OBX_PUBLISH_RELEASE == "true"
+ when: on_success
script:
- - ./gradlew $GITLAB_REPO_ARGS $VERSION_ARGS :objectbox-java:packageJavadocForWeb
+ - ./gradlew $GITLAB_REPO_ARGS :objectbox-java:packageJavadocForWeb
after_script:
- ci/send-to-gchat.sh "$GOOGLE_CHAT_WEBHOOK_JAVA_CI" --thread $CI_COMMIT_SHA "API docs for web available as job artifact $CI_JOB_URL"
artifacts:
paths:
- "objectbox-java/build/dist/objectbox-java-web-api-docs.zip"
+# Trigger Gradle plugin build to test new Maven snapshots of this project
trigger-plugin:
stage: triggers
- except:
- - schedules # Do not trigger when run on schedule, e.g. integ tests have own schedule.
- - publish
+ rules:
+ # Not for tag pipelines
+ - if: $CI_COMMIT_TAG
+ - when: never
+ # Not when publishing a release
+ - if: $OBX_PUBLISH_RELEASE == "true"
+ when: never
+ # Do not trigger publishing of plugin
+ - if: $CI_COMMIT_BRANCH == "publish"
+ when: never
+ # Not for scheduled pipelines where Maven snapshots of this project do not change
+ - if: $CI_PIPELINE_SOURCE == "schedule"
+ when: never
+ # Otherwise, only if no previous stages failed. Also set allow_failure in case branch does not exist downstream.
+ - when: on_success
inherit:
variables: false
- allow_failure: true # Branch might not exist, yet, in plugin project.
+ allow_failure: true # Branch might not exist in plugin project
trigger:
project: objectbox/objectbox-plugin
branch: $CI_COMMIT_BRANCH
diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md
index 4ebced4c..100c5fd0 100644
--- a/.gitlab/merge_request_templates/Default.md
+++ b/.gitlab/merge_request_templates/Default.md
@@ -1,23 +1,28 @@
-## What does this MR do?
+## What does this merge request do?
-
+TODO Link associated issue from title, like: `
#NUMBER`
+
+TODO Briefly list what this merge request is about
## Author's checklist
-- [ ] The MR fully addresses the requirements of the associated task.
-- [ ] I did a self-review of the changes and did not spot any issues. Among others, this includes:
- * I added unit tests for new/changed behavior; all test pass.
- * My code conforms to our coding standards and guidelines.
- * My changes are prepared in a way that makes the review straightforward for the reviewer.
+- [ ] This merge request fully addresses the requirements of the associated task
+- [ ] I did a self-review of the changes and did not spot any issues, among others:
+ - I added unit tests for new or changed behavior; existing and new tests pass
+ - I checked the SpotBugs report for new issues
+ - My code conforms to our coding standards and guidelines
+ - My changes are prepared (focused commits, good messages) so reviewing them is easy for the reviewer
+- [ ] I amended the [changelog](/CHANGELOG.md) if this affects users in any way
+- [ ] I assigned a reviewer to request review
-## Review checklist
+## Reviewer's checklist
-- [ ] I reviewed all changes line-by-line and addressed relevant issues
+- [ ] I reviewed all changes line-by-line and addressed relevant issues. However:
+ - for quickly resolved issues, I considered creating a fixup commit and discussing that, and
+ - instead of many or long comments, I considered a meeting with or a draft commit for the author.
- [ ] The requirements of the associated task are fully met
-- [ ] I can confirm that:
- * CI passes
- * Coverage percentages do not decrease
- * New code conforms to standards and guidelines
- * If applicable, additional checks were done for special code changes (e.g. core performance, binary size, OSS licenses)
-
-/assign me
+- [ ] I can confirm that:
+ - CI passes
+ - If applicable, coverage percentages do not decrease
+ - New code conforms to standards and guidelines
+ - If applicable, additional checks were done for special code changes (e.g. core performance, binary size, OSS licenses)
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..ece02fa5
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,42 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+- Multi-module Gradle build. Core modules live at the repo root: `objectbox-java-api`, `objectbox-java`, `objectbox-kotlin`, `objectbox-rxjava`, `objectbox-rxjava3`, and `objectbox-android`.
+- `objectbox-android` requires the Android SDK; exclude it with `-PexcludeAndroid=true` if the SDK is unavailable.
+- Test modules live under `tests/`, notably `tests/objectbox-java-test` and `tests/test-proguard`.
+- Shared build logic (e.g., publishing conventions) lives in the `build-logic/` included build.
+- Source follows standard Gradle layout per module (e.g., `src/main/java`, `src/main/kotlin`, `src/test/java`).
+- CI config: `.gitlab-ci.yml` (primary; Jenkins is obsolete); helper scripts live in `scripts/`; build outputs go to `build/`.
+
+## Build, Test, and Development Commands
+- `./gradlew clean build` — full build across modules.
+- `./gradlew :objectbox-java:build` — build a single module.
+- `./gradlew :tests:objectbox-java-test:test` — run the main test suite.
+- `./gradlew :tests:objectbox-java-test:test --tests io.objectbox.query.QueryTest` — run a single test class.
+- `./gradlew -version` — verify Gradle/JDK setup.
+- `./scripts/test-with-asan.sh --stacktrace clean build` — CI-style run with ASAN (requires native deps; currently disabled in CI, see issue #273).
+
+## Coding Style & Naming Conventions
+- No repo-wide formatter config is present; follow existing Java/Kotlin style in the touched files and keep changes minimal.
+- Use 4-space indentation and standard Java/Kotlin naming conventions; prefer package naming under `io.objectbox`.
+- New tests in `tests/objectbox-java-test` should name methods as `{attribute}_{queryCondition}_{expectation}` (see `tests/README.md`).
+
+## Testing Guidelines
+- Tests are JUnit-based (see `tests/objectbox-java-test/build.gradle.kts`).
+- Tests run against the real native database library (`io.objectbox:objectbox-linux`/`-macos`/`-windows` from Maven).
+ Snapshot versions of these resolve only via the internal GitLab repository (`gitlabUrl`/`gitlabPrivateToken` Gradle properties, see `settings.gradle.kts`).
+- Place new tests under `tests/objectbox-java-test/src/test/java` (or Kotlin equivalents).
+- Legacy tests may still use the `test*` prefix; new tests should follow the naming convention above.
+
+## Key Classes & Hotspots
+- Core API: `io.objectbox.BoxStore`, `io.objectbox.Box`, and `io.objectbox.Transaction` (see `tests/objectbox-java-test/src/test/java/io/objectbox/BoxStoreTest.java` and `tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java`).
+- Queries: `io.objectbox.query.Query`, `QueryBuilder`, and `QueryCondition` (see `tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java` and `tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java`).
+- Relations: `io.objectbox.relation.ToOne` and `ToMany` (see `tests/objectbox-java-test/src/test/java/io/objectbox/relation/RelationTest.java`).
+- Sync: `io.objectbox.sync.SyncClient`, `SyncServer`, and `SyncCredentials` (see `tests/objectbox-java-test/src/test/java/io/objectbox/sync/SyncTest.java`).
+
+## Commit & Pull Request Guidelines
+- The default branch for development and PRs is `dev`; the `publish` branch is used for releases to Maven Central.
+- Recent history uses a short area prefix, e.g., `QueryTest: use new API...` or `TargetIdProperty: improve...`. Follow this pattern for new commits.
+- Add user-facing changes to the "Next release" section of `CHANGELOG.md`.
+- PRs should include a concise summary, rationale, and the exact test commands run.
+- If changes affect native or CI behavior, call that out explicitly (CI tests against the oldest supported JDK 8 and a recent JDK, see `.gitlab-ci.yml`).
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..3d89fa82
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,318 @@
+# Changelog
+
+Notable changes to the ObjectBox Java library.
+
+For more insights into what changed in the database libraries, [check the ObjectBox C changelog](https://github.com/objectbox/objectbox-c/blob/main/CHANGELOG.md).
+
+## 6.0.0-beta - 2026-07-14
+
+* Update Android and JVM libraries to database version `6.0.0-beta-2026-07-13`
+ * Admin: modernized user interface based on Vue.js 3 and Material Design 3
+ * Admin: optimized pages now loading faster
+ * Android: log messages now use the logcat tag "ObjectBox" (previously "Box")
+ * Internal fixes and improvements
+* objectbox-kotlin: requires at least Kotlin compiler and standard library 2.2.0
+
+### Sync
+
+* **New** [Mesh Sync API](https://sync.objectbox.io/mesh-sync): peer-to-peer (P2P) synchronization between sync clients without a central server.
+ * Attach a `MeshConfig` via `SyncBuilder.mesh()` and query the running mesh via `SyncClient.getMesh()` (state, connected peer count, statistics counters).
+ Mesh configurations are created by ObjectBox platform libraries that provide a mesh network (transport).
+ * New artifact `objectbox-meshsync-android`: provides the mesh network for Android using Google Nearby Connections. Add it next to the Sync variant of the Android library (like `objectbox-sync-android`) and create a mesh configuration with `AndroidMeshSync.createConfig(context, meshId)`. It includes the permissions required by Nearby Connections and the `play-services-nearby` dependency. See [its README for details](objectbox-meshsync-android/README.md).
+
+## 5.4.2 - 2026-05-05
+
+- Update Android and JVM libraries to database version `5.3.2-2026-05-05`
+ - Fixed a corner case with "unique replace on conflict" and absent (e.g. null) sync clock value
+ - Internal fixes and improvements
+
+## 5.4.1 - 2026-03-26
+
+- Update Android and JVM libraries to database version `5.3.1-2026-03-26`
+
+### Sync
+
+- Critical fix for sync clock property handling
+
+## 5.4.0 - 2026-03-24
+
+- Update Android and JVM libraries to database version `5.3.0-2026-03-23`
+- Admin: Status tab "Count and Sizes" and "System and Info" improved
+
+### Sync
+
+- Conflict resolution via the new annotations `@SyncClock` and `@SyncPrecedence` (allows last win with custom conflict
+ resolution)
+- Fix bad Sync client state after multiple full sync messages were interrupted
+- Fix adding indexes to Sync types
+
+## 5.3.0 - 2026-03-10
+
+- Gradle plugin: to apply the plugin, it is no longer necessary to add a manual mapping of the plugin ID in your
+ projects Gradle settings file. This was resolved by publishing Gradle plugin marker artifacts.
+ [#960](https://github.com/objectbox/objectbox-java/issues/960)
+- Gradle plugin: it is no longer necessary to apply the plugin after the dependencies block when manually including
+ ObjectBox dependencies.
+- Update database libraries for Android and JVM to database version `5.2.0-next-2026-03-10`
+- (Minor) performance improvements of HNSW vector search on Linux ARM targets
+
+### Sync
+
+- Sync protocol version 10
+- SyncClient: support updating filter variables. After login, stage updates using put and remove, then schedule to send
+ them to the server with `applyFilterVariables()`.
+- Update client sync filters variables when online
+- Clients report errors to the server
+
+### Migration instructions
+
+#### Drop the Gradle plugin ID mapping
+
+If your `settings.gradle.kts` or `settings.gradle` file contains lines similar to:
+
+```kotlin
+pluginManagement {
+ repositories {
+ mavenCentral()
+ }
+
+ resolutionStrategy {
+ eachPlugin {
+ if (requested.id.id == "io.objectbox") {
+ useModule("io.objectbox:objectbox-gradle-plugin:${requested.version}")
+ }
+ }
+ }
+}
+```
+
+Update them to remove the plugin mapping configuration:
+
+```kotlin
+pluginManagement {
+ repositories {
+ mavenCentral()
+ }
+}
+```
+
+#### Apply the plugin using plugins syntax
+
+If your build scripts are applying the plugin after the dependencies block similar to:
+
+```kotlin
+dependencies {
+ // ...
+}
+
+apply(plugin = "io.objectbox")
+```
+
+Change them to apply the plugin using plugins syntax:
+
+```kotlin
+plugins {
+ id("io.objectbox")
+}
+
+dependencies {
+ // ...
+}
+```
+
+## 5.2.0 - 2026-02-16
+
+- The [ObjectBox Gradle plugin](https://github.com/objectbox/objectbox-java-generator) requires JDK 11 and Android
+ Gradle Plugin 8.1 or newer.
+- Update database libraries for Android and JVM to database version `5.1.1-pre-2026-02-16`.
+- Admin: new "Counts and Sizes" view (via Status page) to give an overview over all types their object counts and sizes
+- Admin: fix reload via browser to stay on the page
+- Admin: schema view to display flags as text
+- Admin: refresh button for the data view
+
+### Sync
+
+- Add simplified `Sync.client(boxStore)` helper method. Move URL and credentials options to builder, add variants that
+ accept multiple URLs and credentials. Deprecate the existing helper methods.
+- Add Sync client builder option to configure Sync behavior using
+ [SyncFlags](objectbox-java/src/main/java/io/objectbox/sync/SyncFlags.java).
+
+## 5.1.0 - 2026-01-26
+
+- Add [ObjectBoxThreadPoolExecutor](objectbox-java/src/main/java/io/objectbox/ObjectBoxThreadPoolExecutor.java), a
+ default implementation of a `ThreadPoolExecutor` that properly cleans up thread-local Box resources.
+- Add methods `newCachedThreadPoolExecutor()` and `newFixedThreadPoolExecutor()` to `BoxStore` to help create instances
+ of `ObjectBoxThreadPoolExecutor` for common uses.
+- Add methods `newCachedThreadPoolDispatcher()` and `newFixedThreadPoolDispatcher()` to the Kotlin extension functions
+ for `BoxStore` to help create common coroutine dispatchers backed by an `ObjectBoxThreadPoolExecutor`.
+- `BoxStore.runInTx` and `callInTx` close a write cursor even if the runnable or callable throws. This would previously
+ result in cursor not closed warnings when the cursor was closed by the finalizer daemon.
+- `BoxStore` no longer gets stuck while closing when deleting transactions created in other threads with open relation
+ cursors.
+- Admin: the Schema view now shows if external name of types and properties if configured.
+- Admin: the Schema view now shows the type as text (e.g. "String") instead of the internal type ID.
+
+### Sync
+
+- New Sync protocol V8: using new clients also requires a server update
+- Remove superfluous sync listener triggers when sync filters "report updates" (SKIP_TX)
+- Sync clients compress earlier: reduces disk storage for outgoing data
+- Reworked certificates for Apple platforms
+- Removed support for older Sync protocol versions before 2024-09; protocol V5+ is now required.
+
+## 5.0.1 - 2025-09-30
+
+- Update runtime libraries for Android and JVM to database version `5.0.0-2025-09-27`.
+- Fix a race condition with a closing store and still active transactions that kept the store from closing.
+ For Android this may fix some rare ANR issues.
+
+### Sync
+
+- Add `filterVariables` option to `SyncClient` builder for [Sync filters](https://sync.objectbox.io/sync-server/sync-filters)
+ introduced in version 5.0.
+
+## 5.0.0 - 2025-09-17
+
+- Includes runtime libraries for Android and JVM with database version `5.0.0-2025-09-16`.
+ - Android: Prior to Android 8.0, don't crash when inserting objects with string lists whose size exceeds the local
+ reference table size. [#1215](https://github.com/objectbox/objectbox-java/issues/1215)
+ - ToOne relations: when deleting an object with an ID larger than the maximum 32-bit unsigned integer
+ (`4_294_967_295`) that is used as the target object of a ToOne, correctly re-set the target ID of the ToOne to
+ `0`. [objectbox-dart#740](https://github.com/objectbox/objectbox-dart/issues/740)
+- When re-creating a `BoxStore` for the same directory and `close()` wasn't called on the previous instance, don't throw
+ an "Another BoxStore is still open for this directory" exception. Note that calling `close()` *is recommended* before
+ creating a new instance. [#1201](https://github.com/objectbox/objectbox-java/issues/1201)
+- When using `BoxStoreBuilder.buildDefault()`, don't leak the Store when setting it as default fails.
+- To help diagnose, print stacks of all threads in the internal thread pool if shutting it down takes too long when
+ closing `BoxStore`.
+- Remove deprecated APIs:
+ - `Query.setParameters` methods that set a single parameter, use the `setParameter` methods instead.
+ - `Box.removeByKeys`, use `Box.removeByIds` instead.
+ - `BoxStore.sizeOnDisk`, use `getDbSize` or `getDbSizeOnDisk` instead which properly handle in-memory databases.
+ - `BoxStoreBuilder.debugTransactions`, use `debugFlags(DebugFlags.LOG_TRANSACTIONS_READ | DebugFlags.LOG_TRANSACTIONS_WRITE)` instead.
+ - `SyncServerBuilder` `peer` configuration options, use the `clusterPeer` options instead.
+ - `io.objectbox.DebugFlags`, use `io.objectbox.config.DebugFlags` instead.
+ - `ValidateOnOpenMode` constants, use `ValidateOnOpenModePages` instead.
+ - DAOcompat compatibility query methods. Use the regular query API instead.
+
+### Sync
+
+- Support Sync server version 5.0.
+ - **User-Specific Data Sync**: support configuring [Sync filter](https://sync.objectbox.io/sync-server/sync-filters)
+ variables on `SyncClient`.
+
+## 4.3.1 - 2025-08-12
+
+- Requires at least Kotlin compiler and standard library 1.7.
+- Data Observers: closing a Query now waits on a running publisher to finish its query, preventing a VM crash. [#1147](https://github.com/objectbox/objectbox-java/issues/1147)
+- Update database libraries for Android and JVM to version `4.3.1` (include database version `4.3.1-2025-08-02`).
+
+## 4.3.0 - 2025-05-13
+
+- Basic support for boolean array properties (`boolean[]` in Java or `BooleanArray` in Kotlin).
+- The Windows database library now statically links the MSVC runtime to avoid crashes in incompatible `msvcp140.dll`
+ shipped with some JDKs.
+- External property types (via [MongoDB connector](https://sync.objectbox.io/mongodb-sync-connector)):
+ - add `JSON_TO_NATIVE` to support sub (embedded/nested) documents/arrays in MongoDB
+ - support ID mapping to UUIDs (v4 and v7)
+- Admin: add class and dependency diagrams to the schema page (view and download).
+- Admin: improved data view for large vectors by displaying only the first elements and the full vector in a dialog.
+- Admin: detects images stored as bytes and shows them as such (PNG, GIF, JPEG, SVG, WEBP).
+
+### Sync
+
+- Add "Log Events" for important server events, which can be viewed on a new Admin page.
+- Detect and ignore changes for objects that were put but were unchanged.
+- The limit for message size was raised to 32 MB.
+- Transactions above the message size limit now already fail on the client (to better enforce the limit).
+
+## 4.2.0 - 2025-03-04
+
+- Add new query conditions `equalKeyValue`, `greaterKeyValue`, `lessKeyValue`, `lessOrEqualKeyValue`, and
+ `greaterOrEqualKeyValue` that are helpful to write complex queries for [string maps](https://docs.objectbox.io/advanced/custom-types#flex-properties).
+ These methods support `String`, `long` and `double` data types for the values in the string map.
+- Deprecate the `containsKeyValue` condition, use the new `equalKeyValue` condition instead.
+- Android: to build, at least Android Plugin 8.1.1 and Gradle 8.2.1 are required.
+
+## 4.1.0 - 2025-01-30
+
+- Vector Search: add new `VectorDistanceType.GEO` distance type to perform vector searches on geographical coordinates.
+ This is particularly useful for location-based applications.
+- Android: require Android 5.0 (API level 21) or higher.
+- Note on Windows JVM: We've seen crashes on Windows when creating a BoxStore on some JVM versions.
+ If this should happen to you, make sure to update your JVM to the latest patch release
+ (8.0.432+6, 11.0.25+9, 17.0.13+11 and 21.0.5+11-LTS are known to work).
+
+### Sync
+
+- Add JWT authentication
+- Sync clients can now send multiple credentials for login
+
+## 4.0.3 - 2024-10-15
+
+- Make closing the Store more robust. In addition to transactions, it also waits for ongoing queries. This is just an
+ additional safety net. Your apps should still make sure to finish all Store operations, like queries, before closing it.
+- [Flex properties](https://docs.objectbox.io/advanced/custom-types#flex-properties) support `null` map and list values.
+- Some minor vector search performance improvements.
+
+### Sync
+
+- **Fix a serious regression, please update as soon as possible.**
+- Add new options, notably for cluster configuration, when building `SyncServer`. Improve documentation.
+ Deprecate the old peer options in favor of the new cluster options.
+- Add `SyncHybrid`, a combination of a Sync client and a Sync server. It can be used in local cluster setups, in
+ which a "hybrid" functions as a client & cluster peer (server).
+
+## 4.0.2 - 2024-08-20
+
+- Add convenience `oneOf` and `notOneOf` conditions that accept `Date` to avoid manual conversion using `getTime()`.
+- When `BoxStore` is closing, briefly wait on active transactions to finish.
+- Guard against crashes when `BoxStore` was closed, but database operations do still occur concurrently (transactions are still active).
+
+## 4.0.1 - 2024-06-03
+
+- Examples: added [Vector Search example](https://github.com/objectbox/objectbox-examples/tree/main/java-main-vector-search) that demonstrates how to perform on-device [approximate nearest neighbor (ANN) search](https://docs.objectbox.io/on-device-vector-search).
+- Revert deprecation of `Box.query()`, it is still useful for queries without any condition.
+- Add note on old query API methods of `QueryBuilder` that they are not recommended for new projects. Use [the new query APIs](https://docs.objectbox.io/queries) instead.
+- Update and expand documentation on `ToOne` and `ToMany`.
+
+## 4.0.0 - Vector Search - 2024-05-16
+
+**ObjectBox now supports** [**Vector Search**](https://docs.objectbox.io/ann-vector-search) to enable efficient similarity searches.
+
+This is particularly useful for AI/ML/RAG applications, e.g. image, audio, or text similarity. Other use cases include semantic search or recommendation engines.
+
+Create a Vector (HNSW) index for a floating point vector property. For example, a `City` with a location vector:
+
+```java
+@Entity
+public class City {
+
+ @HnswIndex(dimensions = 2)
+ float[] location;
+
+}
+```
+
+Perform a nearest neighbor search using the new `nearestNeighbors(queryVector, maxResultCount)` query condition and the new "find with scores" query methods (the score is the distance to the query vector). For example, find the 2 closest cities:
+
+```java
+final float[] madrid = {40.416775F, -3.703790F};
+final Query query = box
+ .query(City_.location.nearestNeighbors(madrid, 2))
+ .build();
+final City closest = query.findWithScores().get(0).get();
+```
+
+For an introduction to Vector Search, more details and other supported languages see the [Vector Search documentation](https://docs.objectbox.io/ann-vector-search).
+
+- BoxStore: deprecated `BoxStore.sizeOnDisk()`. Instead use one of the new APIs to determine the size of a database:
+ - `BoxStore.getDbSize()` which for a file-based database returns the file size and for an in-memory database returns the approximately used memory,
+ - `BoxStore.getDbSizeOnDisk()` which only returns a non-zero size for a file-based database.
+- Query: add properly named `setParameter(prop, value)` methods that only accept a single parameter value, deprecated the old `setParameters(prop, value)` variants.
+- Sync: add `SyncCredentials.userAndPassword(user, password)`.
+- Gradle plugin: the license of the [Gradle plugin](https://github.com/objectbox/objectbox-java-generator) has changed to the GNU Affero General Public License (AGPL).
+
+## Previous versions
+
+See the [Changelogs in the documentation](https://docs.objectbox.io/changelogs).
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..5cbfa334
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+See @AGENTS.md for repository guidelines
diff --git a/Jenkinsfile b/Jenkinsfile
index 2495fb7c..0b821e72 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -6,7 +6,7 @@ String buildsToKeep = '500'
String gradleArgs = '--stacktrace'
boolean isPublish = BRANCH_NAME == 'publish'
-String versionPostfix = isPublish ? '' : BRANCH_NAME // Build script detects empty string as not set.
+String versionSuffix = isPublish ? '' : BRANCH_NAME // Build script detects empty string as not set.
// Note: using single quotes to avoid Groovy String interpolation leaking secrets.
def signingArgs = '-PsigningKeyFile=$SIGNING_FILE -PsigningKeyId=$SIGNING_ID -PsigningPassword=$SIGNING_PWD'
@@ -56,7 +56,7 @@ pipeline {
stage('build-java') {
steps {
- sh "./ci/test-with-asan.sh $gradleArgs $signingArgs $gitlabRepoArgs clean build"
+ sh "./scripts/test-with-asan.sh $gradleArgs $signingArgs $gitlabRepoArgs clean build"
}
post {
always {
@@ -78,7 +78,7 @@ pipeline {
// "|| true" for an OK exit code if no file is found
sh 'rm tests/objectbox-java-test/hs_err_pid*.log || true'
// Note: do not run check task as it includes SpotBugs.
- sh "./ci/test-with-asan.sh $gradleArgs $gitlabRepoArgs clean :tests:objectbox-java-test:test"
+ sh "./scripts/test-with-asan.sh $gradleArgs $gitlabRepoArgs clean :tests:objectbox-java-test:test"
}
post {
always {
@@ -95,7 +95,7 @@ pipeline {
// "|| true" for an OK exit code if no file is found
sh 'rm tests/objectbox-java-test/hs_err_pid*.log || true'
// Note: do not run check task as it includes SpotBugs.
- sh "./ci/test-with-asan.sh $gradleArgs $gitlabRepoArgs clean :tests:objectbox-java-test:test"
+ sh "./scripts/test-with-asan.sh $gradleArgs $gitlabRepoArgs clean :tests:objectbox-java-test:test"
}
post {
always {
@@ -107,7 +107,7 @@ pipeline {
stage('upload-to-internal') {
steps {
- sh "./gradlew $gradleArgs $signingArgs $gitlabRepoArgs -PversionPostFix=$versionPostfix publishMavenJavaPublicationToGitLabRepository"
+ sh "./gradlew $gradleArgs $signingArgs $gitlabRepoArgs -PversionSuffix=$versionSuffix publishMavenJavaPublicationToGitLabRepository"
}
}
diff --git a/README.md b/README.md
index 5b4a5baf..761ae1ad 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+ * Usage requires manually configuring some ObjectBox dependencies, see the
+ * documentation for more details.
+ *
+ *
+ * if (BuildConfig.DEBUG) {
+ * boolean started = new Admin(boxStore).start(this);
+ * Log.i("ObjectBoxAdmin", "Started: " + started);
+ * }
+ *
+ * After {@link #start} is called a notification is displayed. Tap it to open this Admin URL on the device.
+ * Alternatively, look for a logcat message from Admin to obtain the URL. Use {@code adb forward} to access
+ * the URL on your development machine.
+ *
+ * Tapping the notification starts a foreground service to keep this app running in the background.
+ * Stop this keep-alive service from the notification.
+ *
+ * See the web documentation for details.
+ */
+public class Admin {
+
+ private int notificationId;
+
+ /**
+ * Creates a helper to control Admin for the given {@code boxStore}.
+ *
+ * See the {@link Admin class documentation} for details.
+ */
+ public Admin(BoxStore boxStore) {
+ }
+
+ /**
+ * The id passed to {@link android.app.NotificationManager#notify} for the Admin notification.
+ */
+ public int getNotificationId() {
+ return notificationId;
+ }
+
+ /**
+ * The id passed to {@link android.app.NotificationManager#notify} for the Admin notification.
+ */
+ public void setNotificationId(int notificationId) {
+ this.notificationId = notificationId;
+ }
+
+ /**
+ * Starts the ObjectBox Admin HTTP server.
+ *
+ * See the {@link Admin class documentation} for details.
+ */
+ public boolean start(Context context) {
+ return false;
+ }
+
+}
diff --git a/objectbox-android/src/adminExcluded/java/io/objectbox/android/AndroidObjectBrowser.java b/objectbox-android/src/adminExcluded/java/io/objectbox/android/AndroidObjectBrowser.java
new file mode 100644
index 00000000..f5651a56
--- /dev/null
+++ b/objectbox-android/src/adminExcluded/java/io/objectbox/android/AndroidObjectBrowser.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 ObjectBox Ltd.
+ *
+ * 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 io.objectbox.android;
+
+import android.content.Context;
+
+import io.objectbox.BoxStore;
+
+/**
+ * A helper class to start the ObjectBox Admin web app used to browse and gain insights into the database.
+ *
+ * Usage requires manually configuring some ObjectBox dependencies, see the
+ * documentation for more details.
+ *
+ *
+ * if (BuildConfig.DEBUG) {
+ * boolean started = new AndroidObjectBrowser(boxStore).start(this);
+ * Log.i("ObjectBoxAdmin", "Started: " + started);
+ * }
+ *
+ * After {@link #start} is called a notification is displayed. Tap it to open this Admin URL on the device.
+ * Alternatively, look for a logcat message from Admin to obtain the URL. Use {@code adb forward} to access
+ * the URL on your development machine.
+ *
+ * Tapping the notification starts a foreground service to keep this app running in the background.
+ * Stop this keep-alive service from the notification.
+ *
+ * See the web documentation for details.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+@Deprecated
+public class AndroidObjectBrowser extends Admin {
+
+ /**
+ * Creates a helper to control Admin for the given {@code boxStore}.
+ *
+ * See the {@link AndroidObjectBrowser class documentation} for details.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+ @Deprecated
+ public AndroidObjectBrowser(BoxStore boxStore) {
+ super(boxStore);
+ }
+
+ /**
+ * The id passed to {@link android.app.NotificationManager#notify} for the Admin notification.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+ @Deprecated
+ public int getNotificationId() {
+ return super.getNotificationId();
+ }
+
+ /**
+ * The id passed to {@link android.app.NotificationManager#notify} for the Admin notification.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+ @Deprecated
+ public void setNotificationId(int notificationId) {
+ super.setNotificationId(notificationId);
+ }
+
+ /**
+ * Starts the ObjectBox Admin HTTP server.
+ *
+ * See the {@link AndroidObjectBrowser class documentation} for details.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+ @Deprecated
+ public boolean start(Context context) {
+ return super.start(context);
+ }
+}
diff --git a/objectbox-android/src/adminIncluded/AndroidManifest.xml b/objectbox-android/src/adminIncluded/AndroidManifest.xml
new file mode 100644
index 00000000..508901c5
--- /dev/null
+++ b/objectbox-android/src/adminIncluded/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/objectbox-android/src/adminIncluded/java/io/objectbox/android/Admin.java b/objectbox-android/src/adminIncluded/java/io/objectbox/android/Admin.java
new file mode 100644
index 00000000..5bad3ddb
--- /dev/null
+++ b/objectbox-android/src/adminIncluded/java/io/objectbox/android/Admin.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2022 ObjectBox Ltd.
+ *
+ * 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 io.objectbox.android;
+
+import android.Manifest;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import io.objectbox.BoxStore;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+/**
+ * A helper class to start the ObjectBox Admin web app used to browse and gain insights into the database.
+ *
+ * Usage requires manually configuring some ObjectBox dependencies, see the
+ * documentation for more details.
+ *
+ *
+ * if (BuildConfig.DEBUG) {
+ * boolean started = new Admin(boxStore).start(this);
+ * Log.i("ObjectBoxAdmin", "Started: " + started);
+ * }
+ *
+ * After {@link #start} is called a notification is displayed. Tap it to open this Admin URL on the device.
+ * Alternatively, look for a logcat message from Admin to obtain the URL. Use {@code adb forward} to access
+ * the URL on your development machine.
+ *
+ * Tapping the notification starts a foreground service to keep this app running in the background.
+ * Stop this keep-alive service from the notification.
+ *
+ * See the web documentation for details.
+ */
+public class Admin {
+
+ private static final String TAG = "ObjectBoxAdmin";
+ private static final String NOTIFICATION_CHANNEL_ID = "objectbox-browser";
+
+ private final BoxStore boxStore;
+ private int notificationId;
+
+ /**
+ * Creates a helper to control Admin for the given {@code boxStore}.
+ *
+ * See the {@link Admin class documentation} for details.
+ */
+ public Admin(BoxStore boxStore) {
+ this.boxStore = boxStore;
+ }
+
+ /**
+ * The id passed to {@link android.app.NotificationManager#notify} for the Admin notification.
+ */
+ public int getNotificationId() {
+ return notificationId;
+ }
+
+ /**
+ * The id passed to {@link android.app.NotificationManager#notify} for the Admin notification.
+ */
+ public void setNotificationId(int notificationId) {
+ this.notificationId = notificationId;
+ }
+
+ /**
+ * Starts the ObjectBox Admin HTTP server.
+ *
+ * See the {@link Admin class documentation} for details.
+ */
+ public boolean start(Context context) {
+ if (!BoxStore.isObjectBrowserAvailable()) {
+ return false;
+ }
+
+ // compare with objectbox-android-objectbrowser/src/main/AndroidManifest.xml
+ // SecurityException if no INTERNET permission
+ context.enforcePermission(Manifest.permission.INTERNET, Process.myPid(), Process.myUid(), null);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
+ && context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
+ // SecurityException if no FOREGROUND_SERVICE permission
+ context.enforcePermission(Manifest.permission.FOREGROUND_SERVICE, Process.myPid(), Process.myUid(), null);
+ }
+
+ int alreadyRunningPort = boxStore.getObjectBrowserPort();
+ if (alreadyRunningPort != 0) {
+ Log.w(TAG, "ObjectBox Admin is already running at port " + alreadyRunningPort);
+ return false;
+ }
+ String url = boxStore.startObjectBrowser();
+ if (url == null) {
+ return false;
+ }
+ Log.i(TAG, "ObjectBox Admin running at URL: " + url);
+ int port = boxStore.getObjectBrowserPort();
+ Log.i(TAG, "To access the ObjectBox Admin URL on your machine run: adb forward tcp:" + port + " tcp:" + port);
+
+
+ if (notificationId == 0) {
+ notificationId = 19770000 + port;
+ }
+
+ // build intent and show notification
+ Intent intent = new Intent(context, AdminNotificationReceiver.class);
+ intent.setAction(AdminNotificationReceiver.ACTION_KEEP_ALIVE);
+ intent.putExtra(AdminKeepAliveService.EXTRA_KEY_URL, url);
+ intent.putExtra(AdminKeepAliveService.EXTRA_KEY_PORT, port);
+ intent.putExtra(AdminKeepAliveService.EXTRA_KEY_NOTIFICATION_ID, notificationId);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent,
+ buildPendingIntentFlags(PendingIntent.FLAG_CANCEL_CURRENT));
+
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ // Note: may be null in case where notification service is not available on device (e.g. custom Android OS).
+ if (manager != null) {
+ // On Android 13 or newer notifications for apps are turned off by default,
+ // developers need to turn on notifications for the app through system settings
+ // or request the permission from the user.
+ // https://developer.android.com/develop/ui/views/notifications/notification-permission
+ // Already on Android 7 or newer, notifications can been turned off in system settings. This may happen
+ // while developers are testing an app, so also warn in this case.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !manager.areNotificationsEnabled()) {
+ Log.w(TAG, "To use the ObjectBox Admin keep-alive notification turn on notifications for this app");
+ }
+ Notification.Builder builder = buildBaseNotification(context, port, manager);
+ builder.setContentIntent(pendingIntent);
+ manager.notify(notificationId, builder.build());
+ }
+
+ return true;
+ }
+
+ static Notification.Builder buildBaseNotification(Context context, int port, NotificationManager manager) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // Note: IMPORTANCE_LOW so no sound is played to avoid distractions while testing.
+ NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
+ "ObjectBox Admin", NotificationManager.IMPORTANCE_LOW);
+ // if channel already exists, create call will be ignored
+ manager.createNotificationChannel(channel);
+ }
+
+ Notification.Builder builder;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
+ } else {
+ builder = new Notification.Builder(context);
+ }
+
+ builder.setContentTitle(context.getString(R.string.objectbox_objectBrowserNotificationTitle))
+ .setContentText(context.getString(R.string.objectbox_objectBrowserNotificationText, port))
+ .setSmallIcon(R.drawable.objectbox_notification);
+
+ return builder;
+ }
+
+ static Intent viewIntent(String url) {
+ Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ viewIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
+ return viewIntent;
+ }
+
+ /**
+ * Targeting Android 12 requires to mark PendingIntents explicitly as immutable or mutable.
+ * https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability
+ */
+ static int buildPendingIntentFlags(int flags) {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ return flags | PendingIntent.FLAG_IMMUTABLE;
+ } else {
+ return flags;
+ }
+ }
+
+}
diff --git a/objectbox-android/src/adminIncluded/java/io/objectbox/android/AdminKeepAliveService.java b/objectbox-android/src/adminIncluded/java/io/objectbox/android/AdminKeepAliveService.java
new file mode 100644
index 00000000..1d0f1c78
--- /dev/null
+++ b/objectbox-android/src/adminIncluded/java/io/objectbox/android/AdminKeepAliveService.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 ObjectBox Ltd.
+ *
+ * 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 io.objectbox.android;
+
+import android.app.Notification;
+import android.app.Notification.Action.Builder;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.IBinder;
+import android.util.Log;
+
+import javax.annotation.Nullable;
+
+/**
+ * Foreground service to keep app alive which displays a notification to view {@link Admin} URL or stop this service.
+ */
+public class AdminKeepAliveService extends Service {
+
+ private static final String ACTION_STOP = "objectBox_keepAliveStop";
+
+ static final String EXTRA_KEY_PORT = "port";
+ static final String EXTRA_KEY_URL = "url";
+ static final String EXTRA_KEY_NOTIFICATION_ID = "notificationId";
+
+ private static final String TAG = AdminKeepAliveService.class.getSimpleName();
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (ACTION_STOP.equals(intent.getAction())) {
+ Log.d(TAG, "Stopping");
+ stopForeground(true);
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+ String url = intent.getStringExtra(EXTRA_KEY_URL);
+ int port = intent.getIntExtra(EXTRA_KEY_PORT, 0);
+ int notificationId = intent.getIntExtra(EXTRA_KEY_NOTIFICATION_ID, 0);
+
+ if (url != null && url.startsWith("http") && port > 0 && notificationId > 0) {
+ Intent stopIntent = new Intent(this, getClass());
+ stopIntent.setAction(ACTION_STOP);
+ PendingIntent stopPendingIntent = PendingIntent.getService(this, 0, stopIntent,
+ Admin.buildPendingIntentFlags(PendingIntent.FLAG_CANCEL_CURRENT));
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
+ Admin.viewIntent(url), Admin.buildPendingIntentFlags(0));
+
+ NotificationManager manager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification.Builder builder = Admin.buildBaseNotification(this, port, manager);
+ builder.setContentIntent(pendingIntent);
+ // Actually useless because Foreground notifications cannot be deleted
+ builder.setDeleteIntent(stopPendingIntent);
+ builder.addAction(new Builder(R.drawable.objectbox_stop, "Stop", stopPendingIntent).build());
+
+ startForeground(notificationId, builder.getNotification());
+ Log.d(TAG, "Started");
+ return START_REDELIVER_INTENT; // with START_STICKY would not get intent on restart
+ } else {
+ Log.w(TAG, "Ignoring start command due to incomplete data");
+ return START_NOT_STICKY;
+ }
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
diff --git a/objectbox-android/src/adminIncluded/java/io/objectbox/android/AdminNotificationReceiver.java b/objectbox-android/src/adminIncluded/java/io/objectbox/android/AdminNotificationReceiver.java
new file mode 100644
index 00000000..51db3850
--- /dev/null
+++ b/objectbox-android/src/adminIncluded/java/io/objectbox/android/AdminNotificationReceiver.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 ObjectBox Ltd.
+ *
+ * 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 io.objectbox.android;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+
+/**
+ * Handles actions of the {@link Admin} notification.
+ */
+public class AdminNotificationReceiver extends BroadcastReceiver {
+
+ private static final String TAG = AdminNotificationReceiver.class.getSimpleName();
+ static final String ACTION_KEEP_ALIVE = "io.objectbox.action.KEEP_ALIVE";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!ACTION_KEEP_ALIVE.equals(intent.getAction())) {
+ return;
+ }
+ if (!intent.hasExtra(AdminKeepAliveService.EXTRA_KEY_URL)) {
+ Log.w(TAG, "Ignoring keep alive intent due to incomplete data");
+ return;
+ }
+
+ // start foreground service to keep app process alive
+ Intent serviceIntent = new Intent(context, AdminKeepAliveService.class);
+ serviceIntent.putExtras(intent);
+ if (VERSION.SDK_INT >= VERSION_CODES.O) {
+ context.startForegroundService(serviceIntent);
+ } else {
+ context.startService(serviceIntent);
+ }
+
+ // launch browser
+ String url = intent.getStringExtra(AdminKeepAliveService.EXTRA_KEY_URL);
+ context.startActivity(Admin.viewIntent(url));
+ }
+
+}
diff --git a/objectbox-android/src/adminIncluded/java/io/objectbox/android/AndroidObjectBrowser.java b/objectbox-android/src/adminIncluded/java/io/objectbox/android/AndroidObjectBrowser.java
new file mode 100644
index 00000000..f5651a56
--- /dev/null
+++ b/objectbox-android/src/adminIncluded/java/io/objectbox/android/AndroidObjectBrowser.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 ObjectBox Ltd.
+ *
+ * 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 io.objectbox.android;
+
+import android.content.Context;
+
+import io.objectbox.BoxStore;
+
+/**
+ * A helper class to start the ObjectBox Admin web app used to browse and gain insights into the database.
+ *
+ * Usage requires manually configuring some ObjectBox dependencies, see the
+ * documentation for more details.
+ *
+ *
+ * if (BuildConfig.DEBUG) {
+ * boolean started = new AndroidObjectBrowser(boxStore).start(this);
+ * Log.i("ObjectBoxAdmin", "Started: " + started);
+ * }
+ *
+ * After {@link #start} is called a notification is displayed. Tap it to open this Admin URL on the device.
+ * Alternatively, look for a logcat message from Admin to obtain the URL. Use {@code adb forward} to access
+ * the URL on your development machine.
+ *
+ * Tapping the notification starts a foreground service to keep this app running in the background.
+ * Stop this keep-alive service from the notification.
+ *
+ * See the web documentation for details.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+@Deprecated
+public class AndroidObjectBrowser extends Admin {
+
+ /**
+ * Creates a helper to control Admin for the given {@code boxStore}.
+ *
+ * See the {@link AndroidObjectBrowser class documentation} for details.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+ @Deprecated
+ public AndroidObjectBrowser(BoxStore boxStore) {
+ super(boxStore);
+ }
+
+ /**
+ * The id passed to {@link android.app.NotificationManager#notify} for the Admin notification.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+ @Deprecated
+ public int getNotificationId() {
+ return super.getNotificationId();
+ }
+
+ /**
+ * The id passed to {@link android.app.NotificationManager#notify} for the Admin notification.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+ @Deprecated
+ public void setNotificationId(int notificationId) {
+ super.setNotificationId(notificationId);
+ }
+
+ /**
+ * Starts the ObjectBox Admin HTTP server.
+ *
+ * See the {@link AndroidObjectBrowser class documentation} for details.
+ *
+ * @deprecated Use {@link Admin} instead.
+ */
+ @Deprecated
+ public boolean start(Context context) {
+ return super.start(context);
+ }
+}
diff --git a/objectbox-android/src/adminIncluded/res/drawable-hdpi/objectbox_notification.png b/objectbox-android/src/adminIncluded/res/drawable-hdpi/objectbox_notification.png
new file mode 100644
index 00000000..84236e32
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-hdpi/objectbox_notification.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-hdpi/objectbox_stop.png b/objectbox-android/src/adminIncluded/res/drawable-hdpi/objectbox_stop.png
new file mode 100644
index 00000000..dfff26ce
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-hdpi/objectbox_stop.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-mdpi/objectbox_notification.png b/objectbox-android/src/adminIncluded/res/drawable-mdpi/objectbox_notification.png
new file mode 100644
index 00000000..da2b3038
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-mdpi/objectbox_notification.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-mdpi/objectbox_stop.png b/objectbox-android/src/adminIncluded/res/drawable-mdpi/objectbox_stop.png
new file mode 100644
index 00000000..9f6dc9ef
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-mdpi/objectbox_stop.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-xhdpi/objectbox_notification.png b/objectbox-android/src/adminIncluded/res/drawable-xhdpi/objectbox_notification.png
new file mode 100644
index 00000000..0719e380
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-xhdpi/objectbox_notification.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-xhdpi/objectbox_stop.png b/objectbox-android/src/adminIncluded/res/drawable-xhdpi/objectbox_stop.png
new file mode 100644
index 00000000..3ad2c9c4
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-xhdpi/objectbox_stop.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-xxhdpi/objectbox_notification.png b/objectbox-android/src/adminIncluded/res/drawable-xxhdpi/objectbox_notification.png
new file mode 100644
index 00000000..97af3800
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-xxhdpi/objectbox_notification.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-xxhdpi/objectbox_stop.png b/objectbox-android/src/adminIncluded/res/drawable-xxhdpi/objectbox_stop.png
new file mode 100644
index 00000000..801d3411
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-xxhdpi/objectbox_stop.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-xxxhdpi/objectbox_notification.png b/objectbox-android/src/adminIncluded/res/drawable-xxxhdpi/objectbox_notification.png
new file mode 100644
index 00000000..a6bc5520
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-xxxhdpi/objectbox_notification.png differ
diff --git a/objectbox-android/src/adminIncluded/res/drawable-xxxhdpi/objectbox_stop.png b/objectbox-android/src/adminIncluded/res/drawable-xxxhdpi/objectbox_stop.png
new file mode 100644
index 00000000..52393366
Binary files /dev/null and b/objectbox-android/src/adminIncluded/res/drawable-xxxhdpi/objectbox_stop.png differ
diff --git a/objectbox-android/src/adminIncluded/res/values/strings.xml b/objectbox-android/src/adminIncluded/res/values/strings.xml
new file mode 100644
index 00000000..d6de685e
--- /dev/null
+++ b/objectbox-android/src/adminIncluded/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ ObjectBox Admin
+
+ Running on port %1$d. Tap to open and keep app running.
+
diff --git a/objectbox-android/src/main/AndroidManifest.xml b/objectbox-android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..44cfa242
--- /dev/null
+++ b/objectbox-android/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/objectbox-android/src/main/java/io/objectbox/android/AndroidScheduler.java b/objectbox-android/src/main/java/io/objectbox/android/AndroidScheduler.java
new file mode 100644
index 00000000..ac2c358c
--- /dev/null
+++ b/objectbox-android/src/main/java/io/objectbox/android/AndroidScheduler.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 ObjectBox Ltd.
+ *
+ * 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 io.objectbox.android;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+import androidx.annotation.NonNull;
+import io.objectbox.reactive.RunWithParam;
+import io.objectbox.reactive.Scheduler;
+
+/**
+ * A Looper-based Scheduler implementation, see {@link #mainThread()} for most common usage.
+ */
+public class AndroidScheduler extends Handler implements Scheduler {
+ private static AndroidScheduler MAIN_THREAD;
+
+ /**
+ * Returns a Scheduler that runs tasks on Android's main thread.
+ */
+ public static synchronized Scheduler mainThread() {
+ if (MAIN_THREAD == null) {
+ MAIN_THREAD = new AndroidScheduler(Looper.getMainLooper());
+ }
+ return MAIN_THREAD;
+ }
+
+ private final Deque freeRunners = new ArrayDeque<>();
+
+ /**
+ * If you run your own Looper, you can create a custom Scheduler using it.
+ */
+ public AndroidScheduler(Looper looper) {
+ super(looper);
+ }
+
+ // Note: need to cast to RunWithParam