diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 48a4d43..f664c16 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -24,3 +24,84 @@ jobs:
wget https://github.com/codenameone/CodenameOne/raw/refs/heads/master/maven/CodeNameOneBuildClient.jar -O ~/.codenameone/CodeNameOneBuildClient.jar
- name: Build with Maven
run: mvn install
+
+ android-emulator:
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Java 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+
+ - name: Install Android SDK
+ uses: android-actions/setup-android@v3
+ with:
+ packages: |-
+ platform-tools
+ emulator
+ system-images;android-33;google_apis;x86_64
+
+ - name: Enable KVM
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils
+ sudo usermod -aG kvm $USER
+ sudo udevadm control --reload-rules && sudo udevadm trigger
+
+ - name: Build app with Maven Central artifacts
+ run: |
+ # Use local BTDemo directory
+ APP_DIR=$PWD/BTDemo \
+ CODENAMEONE_VERSION=7.0.26 \
+ BUILD_TARGET=android-source \
+ ./scripts/ci/build-thirdparty-app.sh android
+
+ - name: Build APK from Source
+ run: |
+ # The generated project should be in scripts/ci/.thirdparty-app/app/target
+ # We search for it
+
+ # Find the android project directory (contains build.gradle)
+ # We look inside the work dir used by the script
+ ANDROID_PROJECT_DIR=$(find scripts/ci/.thirdparty-app/app/target -name "build.gradle" | grep "android" | xargs dirname)
+
+ if [ -z "$ANDROID_PROJECT_DIR" ]; then
+ echo "Could not find generated Android project"
+ find scripts/ci/.thirdparty-app/app/target
+ exit 1
+ fi
+
+ echo "Found Android project at $ANDROID_PROJECT_DIR"
+ cd "$ANDROID_PROJECT_DIR"
+
+ # Ensure gradlew is executable
+ chmod +x gradlew
+
+ # Build APK
+ ./gradlew assembleDebug
+
+ - name: Boot emulator and run smoke test
+ env:
+ ANDROID_SDK_ROOT: ${{ env.ANDROID_SDK_ROOT }}
+ run: |
+ ./scripts/ci/start-android-emulator.sh
+
+ # Find the generated APK
+ # It should be in the android project dir/app/build/outputs/apk/debug/
+ # But we are relative to root now
+
+ APK_PATH=$(find scripts/ci/.thirdparty-app/app/target -name "*.apk" | head -n 1)
+
+ if [ -z "$APK_PATH" ]; then
+ echo "Could not find built APK"
+ find scripts/ci/.thirdparty-app/app/target
+ exit 1
+ fi
+
+ echo "Installing APK from $APK_PATH"
+ adb install -r "$APK_PATH"
+
+ # Start the app
+ adb shell monkey -p com.codename1.btle 1
diff --git a/BTDemo/CodeNameOneBuildClient.jar b/BTDemo/CodeNameOneBuildClient.jar
deleted file mode 100644
index 54b8e97..0000000
Binary files a/BTDemo/CodeNameOneBuildClient.jar and /dev/null differ
diff --git a/BTDemo/codenameone_settings.properties b/BTDemo/codenameone_settings.properties
new file mode 100644
index 0000000..6816a8b
--- /dev/null
+++ b/BTDemo/codenameone_settings.properties
@@ -0,0 +1,51 @@
+#
+#Thu Dec 25 09:28:09 UTC 2025
+codename1.android.keystore=
+codename1.android.keystoreAlias=
+codename1.android.keystorePassword=
+codename1.arg.android.debug=false
+codename1.arg.android.release=true
+codename1.arg.android.xpermissions=
+codename1.arg.ios.add_libs=;CoreBluetooth.framework;
+codename1.arg.ios.application_exits=false
+codename1.arg.ios.background_modes=,bluetooth-central,bluetooth-peripheral
+codename1.arg.ios.dsym=false
+codename1.arg.ios.includePush=false
+codename1.arg.ios.interface_orientation=UIInterfaceOrientationPortrait\:UIInterfaceOrientationPortraitUpsideDown\:UIInterfaceOrientationLandscapeLeft\:UIInterfaceOrientationLandscapeRight
+codename1.arg.ios.newStorageLocation=true
+codename1.arg.ios.plistInject=NSBluetoothPeripheralUsageDescription${foobarfoo}
+codename1.arg.ios.pods=,Cordova,Cordova ~> 6.1
+codename1.arg.ios.pods.platform=,11.0
+codename1.arg.ios.prerendered_icon=false
+codename1.arg.ios.project_type=ios
+codename1.arg.ios.statusbar_hidden=false
+codename1.arg.ios.testFlight=false
+codename1.arg.j2me.nativeThemeConst=0
+codename1.arg.java.version=8
+codename1.arg.rim.obfuscation=false
+codename1.arg.win.ver=8
+codename1.description=
+codename1.displayName=BTDemo
+codename1.icon=icon.png
+codename1.ios.appid=Q5GHSKAL2F.com.codename1.btle
+codename1.ios.certificate=
+codename1.ios.certificatePassword=
+codename1.ios.debug.certificate=
+codename1.ios.debug.certificatePassword=
+codename1.ios.debug.provision=
+codename1.ios.provision=
+codename1.ios.release.certificate=
+codename1.ios.release.certificatePassword=
+codename1.ios.release.provision=
+codename1.j2me.nativeTheme=nbproject/nativej2me.res
+codename1.languageLevel=5
+codename1.mainName=BTDemo
+codename1.packageName=com.codename1.btle
+codename1.rim.certificatePassword=
+codename1.rim.signtoolCsk=
+codename1.rim.signtoolDb=
+codename1.secondaryTitle=CodenameOne_Template
+codename1.vendor=CodenameOne
+codename1.version=1.0
+foobarfoo=This is a description of what we are going to do
+libVersion=111
diff --git a/BTDemo/dist/BTDemo.jar b/BTDemo/dist/BTDemo.jar
deleted file mode 100644
index 11b38cb..0000000
Binary files a/BTDemo/dist/BTDemo.jar and /dev/null differ
diff --git a/BTDemo/dist/README.TXT b/BTDemo/dist/README.TXT
deleted file mode 100644
index a7a14d3..0000000
--- a/BTDemo/dist/README.TXT
+++ /dev/null
@@ -1,32 +0,0 @@
-========================
-BUILD OUTPUT DESCRIPTION
-========================
-
-When you build an Java application project that has a main class, the IDE
-automatically copies all of the JAR
-files on the projects classpath to your projects dist/lib folder. The IDE
-also adds each of the JAR files to the Class-Path element in the application
-JAR files manifest file (MANIFEST.MF).
-
-To run the project from the command line, go to the dist folder and
-type the following:
-
-java -jar "BTDemo.jar"
-
-To distribute this project, zip up the dist folder (including the lib folder)
-and distribute the ZIP file.
-
-Notes:
-
-* If two JAR files on the project classpath have the same name, only the first
-JAR file is copied to the lib folder.
-* Only JAR files are copied to the lib folder.
-If the classpath contains other types of files or folders, these files (folders)
-are not copied.
-* If a library on the projects classpath also has a Class-Path element
-specified in the manifest,the content of the Class-Path element has to be on
-the projects runtime path.
-* To set a main class in a standard Java project, right-click the project node
-in the Projects window and choose Properties. Then click Run and enter the
-class name in the Main Class field. Alternatively, you can manually type the
-class name in the manifest Main-Class element.
diff --git a/BTDemo/dist/lib/CLDC11.jar b/BTDemo/dist/lib/CLDC11.jar
deleted file mode 100644
index e7cdab1..0000000
Binary files a/BTDemo/dist/lib/CLDC11.jar and /dev/null differ
diff --git a/BTDemo/dist/lib/CodenameOne.jar b/BTDemo/dist/lib/CodenameOne.jar
deleted file mode 100644
index 43a9f4a..0000000
Binary files a/BTDemo/dist/lib/CodenameOne.jar and /dev/null differ
diff --git a/BTDemo/dist/lib/CodenameOne_SRC.zip b/BTDemo/dist/lib/CodenameOne_SRC.zip
deleted file mode 100644
index 0e964f7..0000000
Binary files a/BTDemo/dist/lib/CodenameOne_SRC.zip and /dev/null differ
diff --git a/BTDemo/dist/lib/JavaSE.jar b/BTDemo/dist/lib/JavaSE.jar
deleted file mode 100644
index 261c47e..0000000
Binary files a/BTDemo/dist/lib/JavaSE.jar and /dev/null differ
diff --git a/scripts/ci/build-thirdparty-app.sh b/scripts/ci/build-thirdparty-app.sh
new file mode 100755
index 0000000..81a53d7
--- /dev/null
+++ b/scripts/ci/build-thirdparty-app.sh
@@ -0,0 +1,147 @@
+#!/usr/bin/env bash
+# Simple helper to build a Codename One Maven app using published artifacts
+# from Maven Central. Useful for CI jobs that want to validate the toolchain
+# without building Codename One from source.
+set -euo pipefail
+
+function usage() {
+ cat <<'USAGE'
+Usage: build-thirdparty-app.sh
+
+Options are provided via environment variables:
+ APP_DIR Path to an existing Codename One Maven project. If set,
+ the script builds this directory directly.
+ APP_REPO Git URL for a Codename One Maven project to clone.
+ Ignored when APP_DIR is set.
+ APP_REF Optional git ref (branch, tag, or commit) to check out
+ after cloning APP_REPO.
+ WORK_DIR Temporary workspace for cloned/copied sources. Default:
+ /scripts/ci/.thirdparty-app
+ CODENAMEONE_VERSION Codename One runtime version to request from Maven
+ Central. Defaults to LATEST.
+ CODENAMEONE_PLUGIN_VERSION
+ Codename One Maven plugin version. Defaults to
+ CODENAMEONE_VERSION.
+ BUILD_TARGET Overrides the codename1.buildTarget value passed to Maven.
+ Defaults to android-device for android or ios-source for
+ ios.
+
+Examples:
+ # Build a local Maven app for Android
+ APP_DIR=/path/to/app ./scripts/ci/build-thirdparty-app.sh android
+
+ # Build a remote project for iOS from a specific tag
+ APP_REPO=https://github.com/example/my-cn1-app \
+ APP_REF=v1.2.3 \
+ CODENAMEONE_VERSION=8.0.0 \
+ ./scripts/ci/build-thirdparty-app.sh ios
+
+ # Use the bundled hello-codenameone sample as a fallback
+ ./scripts/ci/build-thirdparty-app.sh android
+USAGE
+}
+
+if [[ ${1:-} == "--help" || ${1:-} == "-h" ]]; then
+ usage
+ exit 0
+fi
+
+TARGET=${1:-android}
+if [[ "$TARGET" != "android" && "$TARGET" != "ios" ]]; then
+ echo "Unsupported target '$TARGET'. Expected 'android' or 'ios'." >&2
+ usage
+ exit 1
+fi
+
+CODENAMEONE_VERSION=${CODENAMEONE_VERSION:-LATEST}
+CODENAMEONE_PLUGIN_VERSION=${CODENAMEONE_PLUGIN_VERSION:-$CODENAMEONE_VERSION}
+WORK_DIR=${WORK_DIR:-"$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)/ci/.thirdparty-app"}
+APP_WORK_DIR="$WORK_DIR/app"
+
+function info() {
+ echo "[build-thirdparty] $*"
+}
+
+function prepare_workspace() {
+ rm -rf "$WORK_DIR"
+ mkdir -p "$WORK_DIR"
+}
+
+function copy_local_app() {
+ local source_dir=$1
+ info "Using local app at $source_dir"
+ cp -R "$source_dir" "$APP_WORK_DIR"
+}
+
+function clone_app() {
+ local repo_url=$1
+ info "Cloning $repo_url"
+ git clone --depth 1 "$repo_url" "$APP_WORK_DIR"
+ if [[ -n ${APP_REF:-} ]]; then
+ pushd "$APP_WORK_DIR" >/dev/null
+ git fetch origin "$APP_REF" --depth 1
+ git checkout "$APP_REF"
+ popd >/dev/null
+ fi
+}
+
+function use_bundled_sample() {
+ local root_dir
+ root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)
+ local sample_dir="$root_dir/scripts/hellocodenameone"
+ info "Falling back to bundled sample at $sample_dir"
+ cp -R "$sample_dir" "$APP_WORK_DIR"
+}
+
+function prepare_app() {
+ prepare_workspace
+ if [[ -n ${APP_DIR:-} ]]; then
+ copy_local_app "$APP_DIR"
+ return
+ fi
+ if [[ -n ${APP_REPO:-} ]]; then
+ clone_app "$APP_REPO"
+ return
+ fi
+ use_bundled_sample
+}
+
+function resolve_maven() {
+ local mvnw="$APP_WORK_DIR/mvnw"
+ if [[ -x "$mvnw" ]]; then
+ echo "$mvnw"
+ else
+ echo "mvn"
+ fi
+}
+
+function build_target() {
+ local mvn_cmd
+ mvn_cmd=$(resolve_maven)
+ local build_target
+ case "$TARGET" in
+ android)
+ build_target=${BUILD_TARGET:-android-device}
+ ;;
+ ios)
+ build_target=${BUILD_TARGET:-ios-source}
+ ;;
+ esac
+
+ pushd "$APP_WORK_DIR" >/dev/null
+ info "Building $TARGET with Codename One $CODENAMEONE_VERSION"
+ "$mvn_cmd" \
+ -B \
+ -U \
+ -DskipTests \
+ -Dcn1.version="$CODENAMEONE_VERSION" \
+ -Dcn1.plugin.version="$CODENAMEONE_PLUGIN_VERSION" \
+ -Dcodename1.platform="$TARGET" \
+ -Dcodename1.buildTarget="$build_target" \
+ package
+ popd >/dev/null
+}
+
+prepare_app
+build_target
+info "Build complete. Output available under $APP_WORK_DIR"
diff --git a/scripts/ci/start-android-emulator.sh b/scripts/ci/start-android-emulator.sh
new file mode 100755
index 0000000..6acb35e
--- /dev/null
+++ b/scripts/ci/start-android-emulator.sh
@@ -0,0 +1,125 @@
+#!/usr/bin/env bash
+# Helper to provision and boot an Android emulator suitable for CI.
+# It installs the requested system image, creates an AVD, boots it
+# headlessly, and waits for boot completion.
+set -euo pipefail
+
+function usage() {
+ cat <<'USAGE'
+Usage: start-android-emulator.sh [--help]
+
+Environment variables:
+ ANDROID_SDK_ROOT Path to the Android SDK (required).
+ AVD_NAME Name for the emulator AVD. Default: cn1-ci-api33.
+ AVD_PACKAGE System image to install. Default:
+ system-images;android-33;google_apis;x86_64
+ AVD_DEVICE Device profile to use. Default: pixel_5
+ EMULATOR_PORT Optional TCP port for the emulator console. Default: 5554
+ ADB_SERVER_PORT Optional TCP port for the adb server. Default: 5037
+ EMULATOR_FLAGS Extra flags passed directly to the emulator binary.
+
+The script ensures the emulator is booted and ready for adb commands.
+It is optimized for CI runners (no-window, KVM, swiftshader GPU).
+USAGE
+}
+
+if [[ ${1:-} == "--help" || ${1:-} == "-h" ]]; then
+ usage
+ exit 0
+fi
+
+if [[ -z ${ANDROID_SDK_ROOT:-} ]]; then
+ echo "ANDROID_SDK_ROOT must be set" >&2
+ exit 1
+fi
+
+AVD_NAME=${AVD_NAME:-cn1-ci-api33}
+AVD_PACKAGE=${AVD_PACKAGE:-system-images;android-33;google_apis;x86_64}
+AVD_DEVICE=${AVD_DEVICE:-pixel_5}
+EMULATOR_PORT=${EMULATOR_PORT:-5554}
+ADB_SERVER_PORT=${ADB_SERVER_PORT:-5037}
+
+PATH="$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$PATH"
+
+function info() {
+ echo "[android-emulator] $*"
+}
+
+function ensure_sdk_tools() {
+ if ! command -v sdkmanager >/dev/null; then
+ echo "sdkmanager not found in ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >&2
+ exit 1
+ fi
+ yes | sdkmanager --licenses >/dev/null
+ sdkmanager --install "platform-tools" "emulator" "$AVD_PACKAGE"
+}
+
+function create_avd() {
+ if avdmanager list avd | grep -q "Name: $AVD_NAME"; then
+ info "AVD $AVD_NAME already exists"
+ return
+ fi
+ echo "no" | avdmanager create avd \
+ --name "$AVD_NAME" \
+ --package "$AVD_PACKAGE" \
+ --device "$AVD_DEVICE" \
+ --force
+}
+
+function start_emulator() {
+ local emulator_bin
+ emulator_bin=$(command -v emulator)
+ if [[ ! -x $emulator_bin ]]; then
+ echo "emulator binary not found" >&2
+ exit 1
+ fi
+ info "Starting emulator $AVD_NAME on port $EMULATOR_PORT"
+ # Avoid stale instances
+ if pgrep -f "-avd $AVD_NAME" >/dev/null; then
+ pkill -9 -f "-avd $AVD_NAME"
+ fi
+
+ # Launch headless emulator
+ "${emulator_bin}" -avd "$AVD_NAME" \
+ -port "$EMULATOR_PORT" \
+ -no-window \
+ -no-audio \
+ -no-boot-anim \
+ -gpu swiftshader_indirect \
+ -accel on \
+ -camera-back none \
+ -camera-front none \
+ -verbose \
+ ${EMULATOR_FLAGS:-} \
+ >"$HOME/.android/avd/$AVD_NAME/emulator.log" 2>&1 &
+}
+
+function wait_for_boot() {
+ export ANDROID_ADB_SERVER_PORT="$ADB_SERVER_PORT"
+ adb start-server >/dev/null
+ adb wait-for-device
+ info "Waiting for boot completion"
+ local booted="0"
+ local attempts=0
+ until [[ "$booted" == "1" ]]; do
+ booted=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
+ sleep 3
+ ((attempts++))
+ if (( attempts > 120 )); then
+ info "Emulator failed to boot"
+ tail -n 200 "$HOME/.android/avd/$AVD_NAME/emulator.log" || true
+ exit 1
+ fi
+ done
+ info "Emulator booted"
+ adb shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true
+ adb shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true
+ adb shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true
+}
+
+info "Using SDK at $ANDROID_SDK_ROOT"
+ensure_sdk_tools
+create_avd
+start_emulator
+wait_for_boot
+info "Emulator $AVD_NAME is ready for use"