diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml
index e56c5d124f5..8d12dac78fd 100644
--- a/.github/codeql/codeql-config.yml
+++ b/.github/codeql/codeql-config.yml
@@ -31,3 +31,10 @@ paths-ignore:
# against the JVM, and writing the casts out would change the bytecode shape
# (the DUP2 of a compound array store) that the comparison is there to cover.
- 'vm/tests/src/test/resources/com/codename1/tools/translator/JsNumericDifferentialApp.java'
+ # The AES of the compatibility tests' headless Codename One implementation,
+ # a test double that hands the IV the code under test chose straight to the
+ # JDK. Those IVs are random (com.codename1.security.SecureRandom, filled by
+ # the JDK's SecureRandom.nextBytes here, or the tests' own nextBytes), but
+ # CodeQL treats a `new byte[n]` filled in another method as a static IV and
+ # reports this sink for each of them.
+ - 'maven/compat-testing/src/main/java/com/codename1/compat/testing/HeadlessAes.java'
diff --git a/CodenameOne/src/com/codename1/location/Location.java b/CodenameOne/src/com/codename1/location/Location.java
index 2815ef35281..9d64567f936 100644
--- a/CodenameOne/src/com/codename1/location/Location.java
+++ b/CodenameOne/src/com/codename1/location/Location.java
@@ -131,7 +131,8 @@ public float getAccuracy() {
}
public void setAccuracy(float accuracy) {
- if (Float.isNaN(accuracy)) {
+ // A NaN reading means the platform has none; keep the previous value.
+ if (!Float.isNaN(accuracy)) {
this.accuracy = accuracy;
}
}
diff --git a/NOTICE b/NOTICE
index 39a3940f1b6..afbcc0dc85d 100644
--- a/NOTICE
+++ b/NOTICE
@@ -177,6 +177,25 @@ NETStandard.Library.Ref is distributed under the MIT license:
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+
+================================================================================
+Android Open Source Project: SensorManager math
+================================================================================
+
+maven/android-compat/src/main/java/android/hardware/SensorMath.java is rewritten
+from the static methods of android.hardware.SensorManager in the Android Open
+Source Project (https://source.android.com), so that rotation matrices,
+orientation angles and altitude computed by Android applications on the
+Codename One compatibility runtime match the platform's. It retains the
+original copyright notice:
+
+ Copyright (C) 2008 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
+
+================================================================================
GroupLayout
================================================================================
diff --git a/docs/developer-guide/Android-Interop.asciidoc b/docs/developer-guide/Android-Interop.asciidoc
index 60ad78dee63..4174ed3461e 100644
--- a/docs/developer-guide/Android-Interop.asciidoc
+++ b/docs/developer-guide/Android-Interop.asciidoc
@@ -165,6 +165,23 @@ view class the Codename One Android runtime implements (android2cn1:E0201)`.
* XML: `getResources().getXml()` returns a pull parser over the compiled
resource, and `Xml.newPullParser()` and `Xml.newSerializer()` read and write
XML text.
+* Room: entities, DAOs, type converters, `@Embedded`, `@Upsert`, `@RawQuery`,
+ `LiveData` queries and migrations. The build generates the `_Impl` classes
+ that the Room annotation processor would, so nothing is found by reflection.
+* WorkManager: one-time and periodic work, chains whose outputs merge into the
+ next worker's input, unique work, retries with a growing delay and `LiveData` status
+ queries. Pending work is saved and resumes on the next run.
+* Location and sensors: `LocationManager` with its listeners, last known and
+ current location, and `SensorManager` with the accelerometer, gyroscope,
+ magnetometer, gravity, rotation vector and orientation, including
+ `getRotationMatrix()` and `getOrientation()`.
+* Services declared in the manifest: started and bound services,
+ `IntentService`, and `startForeground()`, which runs as a Codename One
+ `ForegroundService`. Notifications and channels posted through
+ `NotificationManager` and `NotificationCompat` are shown as local
+ notifications.
+* The `AndroidKeyStore` with AES keys from `KeyGenerator` and
+ `KeyGenParameterSpec`, used through `Cipher` in GCM or CBC mode.
=== AndroidX
@@ -321,11 +338,13 @@ the gallery and automated tests cover a subset of those behaviors.
elements. Code that depends on serialization making a snapshot, creator-side
validation, or restoring state after process death needs an explicit storage
or copy implementation.
-* SQLite uses one Codename One database connection. A competing thread's
- operations can throw `SQLiteDatabaseLockedException` while a transaction is
- open; Android's connection-pool waiting behavior isn't implemented.
- Serialize database work through one worker when sharing a database across
- tasks. Full Android SQLite concurrency parity is outside this release's scope.
+* SQLite uses one Codename One database connection. While one thread's
+ transaction is open, another thread's statements wait for it to end, as on
+ Android, and throw `SQLiteDatabaseLockedException` after ten seconds. On the
+ main thread the wait is two seconds, because the UI is frozen while it
+ waits and the other thread may be waiting for the main thread itself.
+ Threads don't read concurrently: one thread's long transaction holds up every
+ other thread's queries.
* Switching only a locale's script, such as `zh-Hans` to `zh-Hant` with the
same region, updates resource selection but doesn't notify or recreate
existing activities. Already-inflated text and layouts can retain the old
@@ -373,9 +392,32 @@ the gallery and automated tests cover a subset of those behaviors.
* Gradle recompiles all Java sources in an imported Android project after a
source edit, because relocation rewrites the compiler output. Unchanged
builds still use Gradle's up-to-date checks.
-* Content providers, services, broadcast intents from the system and
- notifications through `NotificationManager` aren't available; use the
- corresponding Codename One APIs through native interfaces or directly.
+* Content providers and broadcast intents from the system aren't available;
+ use the corresponding Codename One APIs through native interfaces or
+ directly.
+* `AndroidKeyStore` keys aren't hardware backed or non-extractable. Their
+ material is kept in Codename One's `SecureStorage` without a prompt: the
+ keychain on iOS, and a key of the platform's own keystore on Android. A
+ port without a secure store keeps the material in the application's private
+ storage, which a rooted device or a backup can read.
+ `setUserAuthenticationRequired()` isn't enforced; for a key the user must
+ unlock, use `SecureStorage` with a reason, which prompts for biometrics.
+ GCM supports only 128-bit tags.
+* Room rejects `@Relation`, `suspend` DAO methods, `Flow`, RxJava and Paging
+ return types, `@AutoMigration`, `@DatabaseView` and full-text tables at
+ build time, naming the member. `createFromAsset()` fails when the database
+ is built.
+* A `ListenableWorker` subclass returns a `java.util.concurrent.Future`
+ rather than Guava's `ListenableFuture`; `Worker` subclasses are unaffected.
+ A network constraint is treated as met, and charging, idle, battery and
+ storage constraints are checked only where Codename One's `BackgroundWork`
+ supports them. Work saved by an earlier run resumes once the application
+ calls `WorkManager.getInstance()`.
+* A manifest that asks for location access gets the iOS location usage
+ descriptions, and with `ACCESS_BACKGROUND_LOCATION` or
+ `FOREGROUND_SERVICE_LOCATION` the `location` background mode, added to
+ `codenameone_settings.properties` when the project doesn't set them. Edit
+ the added text to change what the iOS permission prompt says.
* `ViewModelProvider` creates view models without reflection: the build
registers every public, concrete `ViewModel` class whose public constructor
takes nothing, an `Application`, a `SavedStateHandle`, or an `Application`
diff --git a/maven/android-compat/spotbugs-exclude.xml b/maven/android-compat/spotbugs-exclude.xml
index b8a84cd1791..a98bdca4b55 100644
--- a/maven/android-compat/spotbugs-exclude.xml
+++ b/maven/android-compat/spotbugs-exclude.xml
@@ -362,4 +362,13 @@
+
+
+
+
+
diff --git a/maven/android-compat/src/main/java/android/app/ActiveServices.java b/maven/android-compat/src/main/java/android/app/ActiveServices.java
new file mode 100644
index 00000000000..82822c59596
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/app/ActiveServices.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.app;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import com.codename1.androidcompat.runtime.AndroidApp;
+import com.codename1.androidcompat.runtime.AndroidRuntime;
+import com.codename1.androidcompat.runtime.CompatReport;
+import com.codename1.androidcompat.runtime.MainThread;
+import com.codename1.background.ForegroundService;
+import com.codename1.ui.Display;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+/// The services of this application: which are running, started, bound and
+/// in the foreground. `Context.startService`, `bindService` and the rest come
+/// here, as does `Service.stopSelf`.
+///
+/// A service is created with the `new` the build generated for its manifest
+/// `` (`AndroidApp.createService`), so an undeclared class is never
+/// started -- as on Android, where the start answers null.
+///
+/// Every lifecycle callback is posted to the main thread, so `startService`
+/// returns before `onCreate` runs, which is Android's order too.
+public final class ActiveServices {
+
+ private ActiveServices() {
+ }
+
+ static final class Record {
+ final Class> type;
+ final ComponentName name;
+ Service service;
+ boolean started;
+ IBinder binder;
+ boolean binderAsked;
+ boolean rebindOnBind;
+ final List connections = new ArrayList();
+ int foregroundId;
+ ForegroundService foreground;
+ CountDownLatch foregroundDone;
+
+ Record(Class> type, ComponentName name) {
+ this.type = type;
+ this.name = name;
+ }
+ }
+
+ private static final Map, Record> RUNNING = new LinkedHashMap, Record>();
+ private static final Map BINDINGS = new LinkedHashMap();
+
+ // What the callers have asked for so far, kept at call time. The
+ // lifecycle runs later, on the main thread, so a stop or unbind made in
+ // the same frame as its start or bind is judged against these rather than
+ // against a service that is not created yet.
+ //
+ // Callers may be on any thread (a worker calling stopService is
+ // ordinary), so each is an immutable set swapped atomically.
+ private static final CowSet> START_REQUESTED = new CowSet>();
+ private static final CowSet BIND_REQUESTED = new CowSet();
+
+ /// A copy-on-write set: every change publishes a new set with
+ /// compareAndSet, so readers on any thread see a whole one.
+ static final class CowSet {
+ private final java.util.concurrent.atomic.AtomicReference> ref =
+ new java.util.concurrent.atomic.AtomicReference>(new java.util.HashSet());
+
+ boolean contains(T t) {
+ return ref.get().contains(t);
+ }
+
+ void add(T t) {
+ while (true) {
+ java.util.Set cur = ref.get();
+ if (cur.contains(t)) {
+ return;
+ }
+ java.util.Set next = new java.util.HashSet(cur);
+ next.add(t);
+ if (ref.compareAndSet(cur, next)) {
+ return;
+ }
+ }
+ }
+
+ /// True when `t` was there.
+ boolean remove(T t) {
+ while (true) {
+ java.util.Set cur = ref.get();
+ if (!cur.contains(t)) {
+ return false;
+ }
+ java.util.Set next = new java.util.HashSet(cur);
+ next.remove(t);
+ if (ref.compareAndSet(cur, next)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ // ------------------------------------------------------------ Context
+
+ public static ComponentName startService(Context caller, final Intent intent) {
+ final Class> type = resolve(intent);
+ if (type == null) {
+ return null;
+ }
+ final Intent copy = new Intent(intent);
+ final ComponentName name = new ComponentName(caller, type);
+ START_REQUESTED.add(type);
+ MainThread.post(new Runnable() {
+ @Override
+ public void run() {
+ Record r = ensureCreated(type, name);
+ if (r == null) {
+ START_REQUESTED.remove(type);
+ return;
+ }
+ // Again here: a stopSelf that ran between the call and this
+ // delivery cleared it, and this start is still a request.
+ START_REQUESTED.add(type);
+ r.started = true;
+ r.service.onStartCommand(copy, 0, r.service.nextStartId());
+ }
+ });
+ return name;
+ }
+
+ public static boolean stopService(Context caller, Intent intent) {
+ final Class> type = resolve(intent);
+ if (type == null) {
+ return false;
+ }
+ if (!START_REQUESTED.remove(type)) {
+ return false;
+ }
+ MainThread.post(new Runnable() {
+ @Override
+ public void run() {
+ Record rec = RUNNING.get(type);
+ if (rec != null) {
+ rec.started = false;
+ destroyIfIdle(rec);
+ }
+ }
+ });
+ return true;
+ }
+
+ public static boolean bindService(Context caller, Intent intent, final ServiceConnection conn, int flags) {
+ if (conn == null) {
+ throw new IllegalArgumentException("connection is null");
+ }
+ final Class> type = resolve(intent);
+ if (type == null) {
+ return false;
+ }
+ final boolean autoCreate = (flags & Context.BIND_AUTO_CREATE) != 0;
+ if (!autoCreate && RUNNING.get(type) == null) {
+ // Android records the binding and connects once something else
+ // starts the service; a later start does not connect it here.
+ CompatReport.unsupported("bindService", "binding without BIND_AUTO_CREATE to a stopped service");
+ return false;
+ }
+ final Intent copy = new Intent(intent);
+ final ComponentName name = new ComponentName(caller, type);
+ BIND_REQUESTED.add(conn);
+ MainThread.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!BIND_REQUESTED.contains(conn)) {
+ // Unbound before it connected: Android delivers nothing.
+ return;
+ }
+ Record r = ensureCreated(type, name);
+ if (r == null) {
+ if (!BINDINGS.containsKey(conn)) {
+ // Only when no live binding of this connection
+ // owns the record.
+ BIND_REQUESTED.remove(conn);
+ }
+ return;
+ }
+ if (BINDINGS.containsKey(conn)) {
+ return;
+ }
+ if (!r.binderAsked) {
+ r.binderAsked = true;
+ r.binder = r.service.onBind(copy);
+ } else if (r.rebindOnBind && r.connections.isEmpty()) {
+ r.rebindOnBind = false;
+ r.service.onRebind(copy);
+ }
+ r.connections.add(conn);
+ BINDINGS.put(conn, r);
+ if (r.binder == null) {
+ conn.onNullBinding(r.name);
+ } else {
+ conn.onServiceConnected(r.name, r.binder);
+ }
+ }
+ });
+ return true;
+ }
+
+ public static void unbindService(Context caller, final ServiceConnection conn) {
+ if (!BIND_REQUESTED.remove(conn)) {
+ throw new IllegalArgumentException("Service not registered: " + conn);
+ }
+ MainThread.post(new Runnable() {
+ @Override
+ public void run() {
+ Record r = BINDINGS.remove(conn);
+ if (r == null) {
+ return;
+ }
+ r.connections.remove(conn);
+ // An unbind is not a disconnect: Android calls no callback.
+ if (r.connections.isEmpty() && r.service != null) {
+ r.rebindOnBind = r.service.onUnbind(new Intent().setClassName(r.name.getPackageName(),
+ r.name.getClassName()));
+ destroyIfIdle(r);
+ }
+ }
+ });
+ }
+
+ // ------------------------------------------------------------- Service
+
+ /// May be called from the service's own worker thread (IntentService
+ /// does), so the start id is compared on the main thread, in order with
+ /// the starts: a start delivered between this call and the stop keeps the
+ /// service running, as on Android. The answer is the call-time view.
+ static boolean stopSelf(final Service s, final int startId) {
+ MainThread.post(new Runnable() {
+ @Override
+ public void run() {
+ Record r = recordOf(s);
+ if (r == null || (startId >= 0 && startId != s.lastStartId())) {
+ return;
+ }
+ r.started = false;
+ START_REQUESTED.remove(r.type);
+ destroyIfIdle(r);
+ }
+ });
+ return startId < 0 || startId == s.lastStartId();
+ }
+
+ static void startForeground(Service s, int id, Notification n) {
+ Record r = recordOf(s);
+ if (r == null || n == null) {
+ return;
+ }
+ r.foregroundId = id;
+ String title = text(n.title());
+ String body = text(n.body());
+ if (r.foreground != null) {
+ r.foreground.updateNotification(title, body);
+ return;
+ }
+ if (!Display.isInitialized()) {
+ return;
+ }
+ // Codename One's foreground service lasts as long as its task: this
+ // one parks a background thread until the service leaves the
+ // foreground, which is when Android takes the notification away.
+ final CountDownLatch done = new CountDownLatch(1);
+ r.foregroundDone = done;
+ String channel = n.getChannelId() == null ? NotificationChannel.DEFAULT_CHANNEL_ID : n.getChannelId();
+ r.foreground = ForegroundService.start(channel, title, body, null, new ForegroundService.Task() {
+ @Override
+ public void run(ForegroundService service) {
+ try {
+ done.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ }
+
+ static void stopForeground(Service s) {
+ Record r = recordOf(s);
+ if (r != null) {
+ endForeground(r);
+ }
+ }
+
+ /// `NotificationManager.notify` with the id of a running foreground
+ /// service updates that service's notification.
+ static boolean updateForegroundNotification(int id, Notification n) {
+ for (Record r : RUNNING.values()) {
+ if (r.foreground != null && r.foregroundId == id) {
+ r.foreground.updateNotification(text(n.title()), text(n.body()));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// Whether `type` is running; for tests and diagnostics.
+ public static boolean isRunning(Class> type) {
+ return RUNNING.containsKey(type);
+ }
+
+ // ------------------------------------------------------------- helpers
+
+ private static String text(CharSequence cs) {
+ return cs == null ? "" : cs.toString();
+ }
+
+ private static Record recordOf(Service s) {
+ for (Record r : RUNNING.values()) {
+ if (r.service == s) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ private static Class> resolve(Intent intent) {
+ if (intent == null) {
+ throw new NullPointerException("intent");
+ }
+ AndroidRuntime rt = AndroidRuntime.getInstance();
+ if (rt == null) {
+ throw new IllegalStateException("The Android runtime is not installed");
+ }
+ AndroidApp app = rt.getApp();
+ Class> type = intent.getComponentClass();
+ if (type == null && intent.getComponent() != null) {
+ type = app.serviceClass(intent.getComponent().getClassName());
+ }
+ if (type == null) {
+ // Android has required explicit service intents since API 21.
+ throw new IllegalArgumentException("Service Intent must be explicit: " + intent);
+ }
+ if (!app.isService(type)) {
+ CompatReport.unsupported("service", type.getName() + " (not declared in the manifest)");
+ return null;
+ }
+ return type;
+ }
+
+ private static Record ensureCreated(Class> type, ComponentName name) {
+ Record r = RUNNING.get(type);
+ if (r != null) {
+ return r;
+ }
+ AndroidRuntime rt = AndroidRuntime.getInstance();
+ Service s = rt.getApp().createService(type);
+ if (s == null) {
+ CompatReport.unsupported("service", type.getName() + " (no generated factory entry)");
+ return null;
+ }
+ r = new Record(type, name);
+ r.service = s;
+ RUNNING.put(type, r);
+ s.attachForRuntime(rt.getApplication(), rt.getApplication());
+ s.onCreate();
+ return r;
+ }
+
+ private static void destroyIfIdle(Record r) {
+ if (r.started || !r.connections.isEmpty() || RUNNING.get(r.type) != r) {
+ return;
+ }
+ RUNNING.remove(r.type);
+ // A destroyed service is not started, whatever re-added it.
+ START_REQUESTED.remove(r.type);
+ endForeground(r);
+ r.service.onDestroy();
+ }
+
+ private static void endForeground(Record r) {
+ if (r.foregroundDone != null) {
+ r.foregroundDone.countDown();
+ r.foregroundDone = null;
+ }
+ if (r.foreground != null) {
+ r.foreground.stop();
+ r.foreground = null;
+ }
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/app/IntentService.java b/maven/android-compat/src/main/java/android/app/IntentService.java
new file mode 100644
index 00000000000..2baeaa4ac8d
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/app/IntentService.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.app;
+
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+/// Deprecated on Android, still common: each start is handled in turn on one
+/// worker thread, and the service stops itself when the queue is empty.
+public abstract class IntentService extends Service {
+
+ private final String name;
+ private Looper serviceLooper;
+ private Handler serviceHandler;
+ private boolean redelivery;
+
+ public IntentService(String name) {
+ this.name = name;
+ }
+
+ public void setIntentRedelivery(boolean enabled) {
+ redelivery = enabled;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ HandlerThread thread = new HandlerThread("IntentService[" + name + "]");
+ thread.start();
+ serviceLooper = thread.getLooper();
+ serviceHandler = new Handler(serviceLooper) {
+ @Override
+ public void handleMessage(Message msg) {
+ onHandleIntent((Intent) msg.obj);
+ stopSelf(msg.arg1);
+ }
+ };
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ if (serviceHandler == null) {
+ // A subclass overrode onCreate without calling super, which
+ // fails the same way on Android.
+ throw new IllegalStateException(getClass().getName() + ".onCreate() did not call super.onCreate()");
+ }
+ Message msg = serviceHandler.obtainMessage();
+ msg.arg1 = startId;
+ msg.obj = intent;
+ serviceHandler.sendMessage(msg);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ onStart(intent, startId);
+ return redelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (serviceLooper != null) {
+ serviceLooper.quit();
+ }
+ }
+
+ @Override
+ public android.os.IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ protected abstract void onHandleIntent(Intent intent);
+}
diff --git a/maven/android-compat/src/main/java/android/app/Notification.java b/maven/android-compat/src/main/java/android/app/Notification.java
new file mode 100644
index 00000000000..e2a696ae096
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/app/Notification.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.app;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/// A notification as built by [Notification.Builder] (or AndroidX's
+/// `NotificationCompat.Builder`). [NotificationManager] posts it as a Codename
+/// One `LocalNotification`, and `Service.startForeground` shows it as the
+/// foreground service's notification. Title, text, channel, group, ongoing,
+/// progress and the badge number carry over; styles contribute their big text
+/// as the body; actions, custom views and large icons are kept but not shown.
+public class Notification {
+
+ public static final int FLAG_SHOW_LIGHTS = 0x00000001;
+ public static final int FLAG_ONGOING_EVENT = 0x00000002;
+ public static final int FLAG_INSISTENT = 0x00000004;
+ public static final int FLAG_ONLY_ALERT_ONCE = 0x00000008;
+ public static final int FLAG_AUTO_CANCEL = 0x00000010;
+ public static final int FLAG_NO_CLEAR = 0x00000020;
+ public static final int FLAG_FOREGROUND_SERVICE = 0x00000040;
+ public static final int FLAG_GROUP_SUMMARY = 0x00000200;
+
+ public static final int DEFAULT_ALL = ~0;
+ public static final int DEFAULT_SOUND = 1;
+ public static final int DEFAULT_VIBRATE = 2;
+ public static final int DEFAULT_LIGHTS = 4;
+
+ public static final int PRIORITY_DEFAULT = 0;
+ public static final int PRIORITY_LOW = -1;
+ public static final int PRIORITY_MIN = -2;
+ public static final int PRIORITY_HIGH = 1;
+ public static final int PRIORITY_MAX = 2;
+
+ public static final int VISIBILITY_PUBLIC = 1;
+ public static final int VISIBILITY_PRIVATE = 0;
+ public static final int VISIBILITY_SECRET = -1;
+
+ public static final int FOREGROUND_SERVICE_DEFAULT = 0;
+ public static final int FOREGROUND_SERVICE_IMMEDIATE = 1;
+ public static final int FOREGROUND_SERVICE_DEFERRED = 2;
+
+ public static final String CATEGORY_CALL = "call";
+ public static final String CATEGORY_NAVIGATION = "navigation";
+ public static final String CATEGORY_MESSAGE = "msg";
+ public static final String CATEGORY_EMAIL = "email";
+ public static final String CATEGORY_EVENT = "event";
+ public static final String CATEGORY_PROMO = "promo";
+ public static final String CATEGORY_ALARM = "alarm";
+ public static final String CATEGORY_PROGRESS = "progress";
+ public static final String CATEGORY_SOCIAL = "social";
+ public static final String CATEGORY_ERROR = "err";
+ public static final String CATEGORY_TRANSPORT = "transport";
+ public static final String CATEGORY_SYSTEM = "sys";
+ public static final String CATEGORY_SERVICE = "service";
+ public static final String CATEGORY_REMINDER = "reminder";
+ public static final String CATEGORY_RECOMMENDATION = "recommendation";
+ public static final String CATEGORY_STATUS = "status";
+ public static final String CATEGORY_WORKOUT = "workout";
+ public static final String CATEGORY_LOCATION_SHARING = "location_sharing";
+
+ public static final String EXTRA_TITLE = "android.title";
+ public static final String EXTRA_TEXT = "android.text";
+ public static final String EXTRA_SUB_TEXT = "android.subText";
+ public static final String EXTRA_BIG_TEXT = "android.bigText";
+ public static final String EXTRA_PROGRESS = "android.progress";
+ public static final String EXTRA_PROGRESS_MAX = "android.progressMax";
+ public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate";
+
+ public long when;
+ public int icon;
+ public int number;
+ public CharSequence tickerText;
+ public PendingIntent contentIntent;
+ public PendingIntent deleteIntent;
+ public int flags;
+ public int defaults;
+ public int priority;
+ public int visibility;
+ public int color;
+ public String category;
+ public Bundle extras = new Bundle();
+ public Action[] actions;
+
+ String channelId;
+ String group;
+ boolean groupSummary;
+ int progress;
+ int progressMax;
+ boolean progressIndeterminate;
+ long timeoutAfter;
+
+ public Notification() {
+ when = System.currentTimeMillis();
+ }
+
+ public String getChannelId() {
+ return channelId;
+ }
+
+ public String getGroup() {
+ return group;
+ }
+
+ public long getTimeoutAfter() {
+ return timeoutAfter;
+ }
+
+ public CharSequence title() {
+ return extras.getCharSequence(EXTRA_TITLE);
+ }
+
+ /// The body a local notification shows: a style's big text when there is
+ /// one, otherwise the content text.
+ CharSequence body() {
+ CharSequence big = extras.getCharSequence(EXTRA_BIG_TEXT);
+ return big != null ? big : extras.getCharSequence(EXTRA_TEXT);
+ }
+
+ /// This notification as a Codename One local notification with `id`.
+ public com.codename1.notifications.LocalNotification toLocal(String id) {
+ com.codename1.notifications.LocalNotification ln = new com.codename1.notifications.LocalNotification();
+ ln.setId(id);
+ CharSequence t = title();
+ CharSequence b = body();
+ ln.setAlertTitle(t == null ? "" : t.toString());
+ ln.setAlertBody(b == null ? "" : b.toString());
+ if (channelId != null) {
+ ln.setChannelId(channelId);
+ }
+ if (group != null) {
+ ln.setGroup(group);
+ ln.setGroupSummary(groupSummary);
+ }
+ if (number > 0) {
+ ln.setBadgeNumber(number);
+ }
+ ln.setOngoing((flags & FLAG_ONGOING_EVENT) != 0);
+ // Codename One shows a determinate bar only; an indeterminate one is
+ // left off rather than drawn as empty.
+ if (progressMax > 0 && !progressIndeterminate) {
+ ln.setProgress(progressMax, progress);
+ }
+ return ln;
+ }
+
+ /// A button on the notification. Kept so code that adds one compiles;
+ /// Codename One local notifications have no buttons to show it on.
+ public static class Action {
+ public int icon;
+ public CharSequence title;
+ public PendingIntent actionIntent;
+
+ public Action(int icon, CharSequence title, PendingIntent intent) {
+ this.icon = icon;
+ this.title = title;
+ this.actionIntent = intent;
+ }
+
+ public static final class Builder {
+ private final Action action;
+
+ public Builder(int icon, CharSequence title, PendingIntent intent) {
+ action = new Action(icon, title, intent);
+ }
+
+ public Builder addExtras(Bundle extras) {
+ return this;
+ }
+
+ public Builder setAllowGeneratedReplies(boolean allow) {
+ return this;
+ }
+
+ public Action build() {
+ return action;
+ }
+ }
+ }
+
+ /// Base of the expanded layouts. Each contributes to the extras a local
+ /// notification reads.
+ public abstract static class Style {
+ protected Builder mBuilder;
+
+ public void setBuilder(Builder builder) {
+ if (mBuilder != builder) {
+ mBuilder = builder;
+ if (builder != null) {
+ builder.setStyle(this);
+ }
+ }
+ }
+
+ void apply(Notification n) {
+ }
+
+ public Notification build() {
+ return mBuilder == null ? null : mBuilder.build();
+ }
+ }
+
+ public static class BigTextStyle extends Style {
+ private CharSequence bigText;
+ private CharSequence bigTitle;
+
+ public BigTextStyle() {
+ }
+
+ public BigTextStyle(Builder builder) {
+ setBuilder(builder);
+ }
+
+ public BigTextStyle bigText(CharSequence cs) {
+ bigText = cs;
+ return this;
+ }
+
+ public BigTextStyle setBigContentTitle(CharSequence title) {
+ bigTitle = title;
+ return this;
+ }
+
+ public BigTextStyle setSummaryText(CharSequence cs) {
+ return this;
+ }
+
+ @Override
+ void apply(Notification n) {
+ if (bigText != null) {
+ n.extras.putCharSequence(EXTRA_BIG_TEXT, bigText);
+ }
+ if (bigTitle != null) {
+ n.extras.putCharSequence(EXTRA_TITLE, bigTitle);
+ }
+ }
+ }
+
+ public static class InboxStyle extends Style {
+ private final List lines = new ArrayList();
+
+ public InboxStyle() {
+ }
+
+ public InboxStyle(Builder builder) {
+ setBuilder(builder);
+ }
+
+ public InboxStyle addLine(CharSequence cs) {
+ lines.add(cs);
+ return this;
+ }
+
+ public InboxStyle setBigContentTitle(CharSequence title) {
+ return this;
+ }
+
+ public InboxStyle setSummaryText(CharSequence cs) {
+ return this;
+ }
+
+ @Override
+ void apply(Notification n) {
+ if (!lines.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < lines.size(); i++) {
+ if (i > 0) {
+ sb.append('\n');
+ }
+ sb.append(lines.get(i));
+ }
+ n.extras.putCharSequence(EXTRA_BIG_TEXT, sb.toString());
+ }
+ }
+ }
+
+ public static class Builder {
+ private final Notification n = new Notification();
+ private final List actions = new ArrayList();
+ private Style style;
+
+ public Builder(Context context, String channelId) {
+ n.channelId = channelId;
+ }
+
+ /// The pre-Oreo constructor: no channel.
+ public Builder(Context context) {
+ this(context, null);
+ }
+
+ public Builder setContentTitle(CharSequence title) {
+ n.extras.putCharSequence(EXTRA_TITLE, title);
+ return this;
+ }
+
+ public Builder setContentText(CharSequence text) {
+ n.extras.putCharSequence(EXTRA_TEXT, text);
+ return this;
+ }
+
+ public Builder setSubText(CharSequence text) {
+ n.extras.putCharSequence(EXTRA_SUB_TEXT, text);
+ return this;
+ }
+
+ public Builder setSmallIcon(int icon) {
+ n.icon = icon;
+ return this;
+ }
+
+ public Builder setLargeIcon(android.graphics.Bitmap b) {
+ return this;
+ }
+
+ public Builder setTicker(CharSequence tickerText) {
+ n.tickerText = tickerText;
+ return this;
+ }
+
+ public Builder setWhen(long when) {
+ n.when = when;
+ return this;
+ }
+
+ public Builder setShowWhen(boolean show) {
+ return this;
+ }
+
+ public Builder setUsesChronometer(boolean b) {
+ return this;
+ }
+
+ public Builder setNumber(int number) {
+ n.number = number;
+ return this;
+ }
+
+ public Builder setContentIntent(PendingIntent intent) {
+ n.contentIntent = intent;
+ return this;
+ }
+
+ public Builder setDeleteIntent(PendingIntent intent) {
+ n.deleteIntent = intent;
+ return this;
+ }
+
+ public Builder setFullScreenIntent(PendingIntent intent, boolean highPriority) {
+ return this;
+ }
+
+ private Builder flag(int mask, boolean value) {
+ if (value) {
+ n.flags |= mask;
+ } else {
+ n.flags &= ~mask;
+ }
+ return this;
+ }
+
+ public Builder setOngoing(boolean ongoing) {
+ return flag(FLAG_ONGOING_EVENT, ongoing);
+ }
+
+ public Builder setAutoCancel(boolean autoCancel) {
+ return flag(FLAG_AUTO_CANCEL, autoCancel);
+ }
+
+ public Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
+ return flag(FLAG_ONLY_ALERT_ONCE, onlyAlertOnce);
+ }
+
+ public Builder setPriority(int pri) {
+ n.priority = pri;
+ return this;
+ }
+
+ public Builder setCategory(String category) {
+ n.category = category;
+ return this;
+ }
+
+ public Builder setVisibility(int visibility) {
+ n.visibility = visibility;
+ return this;
+ }
+
+ public Builder setColor(int argb) {
+ n.color = argb;
+ return this;
+ }
+
+ public Builder setColorized(boolean colorize) {
+ return this;
+ }
+
+ public Builder setDefaults(int defaults) {
+ n.defaults = defaults;
+ return this;
+ }
+
+ public Builder setSound(android.net.Uri sound) {
+ return this;
+ }
+
+ public Builder setVibrate(long[] pattern) {
+ return this;
+ }
+
+ public Builder setLights(int argb, int onMs, int offMs) {
+ return this;
+ }
+
+ public Builder setGroup(String groupKey) {
+ n.group = groupKey;
+ return this;
+ }
+
+ public Builder setGroupSummary(boolean isGroupSummary) {
+ n.groupSummary = isGroupSummary;
+ return flag(FLAG_GROUP_SUMMARY, isGroupSummary);
+ }
+
+ public Builder setSortKey(String sortKey) {
+ return this;
+ }
+
+ public Builder setChannelId(String channelId) {
+ n.channelId = channelId;
+ return this;
+ }
+
+ public Builder setTimeoutAfter(long durationMs) {
+ n.timeoutAfter = durationMs;
+ return this;
+ }
+
+ public Builder setProgress(int max, int progress, boolean indeterminate) {
+ n.progressMax = max;
+ n.progress = progress;
+ n.progressIndeterminate = indeterminate;
+ n.extras.putInt(EXTRA_PROGRESS, progress);
+ n.extras.putInt(EXTRA_PROGRESS_MAX, max);
+ n.extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, indeterminate);
+ return this;
+ }
+
+ public Builder setForegroundServiceBehavior(int behavior) {
+ return this;
+ }
+
+ public Builder setLocalOnly(boolean localOnly) {
+ return this;
+ }
+
+ public Builder setStyle(Style s) {
+ if (style != s) {
+ style = s;
+ if (s != null) {
+ s.setBuilder(this);
+ }
+ }
+ return this;
+ }
+
+ public Builder addAction(int icon, CharSequence title, PendingIntent intent) {
+ actions.add(new Action(icon, title, intent));
+ return this;
+ }
+
+ public Builder addAction(Action action) {
+ actions.add(action);
+ return this;
+ }
+
+ public Builder setExtras(Bundle extras) {
+ if (extras != null) {
+ n.extras.putAll(extras);
+ }
+ return this;
+ }
+
+ public Bundle getExtras() {
+ return n.extras;
+ }
+
+ public Notification build() {
+ Notification out = copy(n);
+ out.actions = actions.toArray(new Action[actions.size()]);
+ if (style != null) {
+ style.apply(out);
+ }
+ return out;
+ }
+
+ /// Deprecated alias of [#build()].
+ public Notification getNotification() {
+ return build();
+ }
+
+ private static Notification copy(Notification src) {
+ Notification out = new Notification();
+ out.when = src.when;
+ out.icon = src.icon;
+ out.number = src.number;
+ out.tickerText = src.tickerText;
+ out.contentIntent = src.contentIntent;
+ out.deleteIntent = src.deleteIntent;
+ out.flags = src.flags;
+ out.defaults = src.defaults;
+ out.priority = src.priority;
+ out.visibility = src.visibility;
+ out.color = src.color;
+ out.category = src.category;
+ out.extras = new Bundle(src.extras);
+ out.channelId = src.channelId;
+ out.group = src.group;
+ out.groupSummary = src.groupSummary;
+ out.progress = src.progress;
+ out.progressMax = src.progressMax;
+ out.progressIndeterminate = src.progressIndeterminate;
+ out.timeoutAfter = src.timeoutAfter;
+ return out;
+ }
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/app/NotificationChannel.java b/maven/android-compat/src/main/java/android/app/NotificationChannel.java
new file mode 100644
index 00000000000..789177bab5d
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/app/NotificationChannel.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.app;
+
+/// A notification channel. [NotificationManager#createNotificationChannel]
+/// registers it as a Codename One `NotificationChannelBuilder`, which is a real
+/// channel on Android and a no-op elsewhere.
+public final class NotificationChannel {
+
+ public static final String DEFAULT_CHANNEL_ID = "miscellaneous";
+
+ private final String id;
+ private CharSequence name;
+ private int importance;
+ private String description;
+ private String group;
+ private boolean showBadge = true;
+ private boolean lights;
+ private int lightColor;
+ private boolean vibration;
+ private long[] vibrationPattern;
+ private int lockscreenVisibility = Notification.VISIBILITY_PRIVATE;
+
+ public NotificationChannel(String id, CharSequence name, int importance) {
+ this.id = id;
+ this.name = name;
+ this.importance = importance;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public CharSequence getName() {
+ return name;
+ }
+
+ public void setName(CharSequence name) {
+ this.name = name;
+ }
+
+ public int getImportance() {
+ return importance;
+ }
+
+ public void setImportance(int importance) {
+ this.importance = importance;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getGroup() {
+ return group;
+ }
+
+ public void setGroup(String groupId) {
+ group = groupId;
+ }
+
+ public boolean canShowBadge() {
+ return showBadge;
+ }
+
+ public void setShowBadge(boolean showBadge) {
+ this.showBadge = showBadge;
+ }
+
+ public boolean shouldShowLights() {
+ return lights;
+ }
+
+ public void enableLights(boolean lights) {
+ this.lights = lights;
+ }
+
+ public int getLightColor() {
+ return lightColor;
+ }
+
+ public void setLightColor(int argb) {
+ lightColor = argb;
+ }
+
+ public boolean shouldVibrate() {
+ return vibration;
+ }
+
+ public void enableVibration(boolean vibration) {
+ this.vibration = vibration;
+ }
+
+ public long[] getVibrationPattern() {
+ return vibrationPattern;
+ }
+
+ public void setVibrationPattern(long[] pattern) {
+ vibrationPattern = pattern;
+ vibration = pattern != null && pattern.length > 0;
+ }
+
+ public int getLockscreenVisibility() {
+ return lockscreenVisibility;
+ }
+
+ public void setLockscreenVisibility(int visibility) {
+ lockscreenVisibility = visibility;
+ }
+
+ /// Sounds name an Android `Uri` and `AudioAttributes`; the channel keeps
+ /// the platform's default sound.
+ public void setSound(android.net.Uri sound, Object audioAttributes) {
+ }
+
+ public void setBypassDnd(boolean bypassDnd) {
+ }
+
+ com.codename1.notifications.NotificationChannelBuilder toBuilder() {
+ com.codename1.notifications.NotificationChannelBuilder b =
+ new com.codename1.notifications.NotificationChannelBuilder(id, name == null ? id : name.toString());
+ // Android's IMPORTANCE_* and Codename One's share their numbering.
+ b.importance(importance);
+ if (description != null) {
+ b.description(description);
+ }
+ if (group != null) {
+ b.group(group);
+ }
+ b.showBadge(showBadge);
+ b.enableLights(lights);
+ if (lightColor != 0) {
+ b.lightColor(lightColor);
+ }
+ b.enableVibration(vibration);
+ if (vibrationPattern != null) {
+ b.vibrationPattern(vibrationPattern);
+ }
+ b.lockscreenVisibility(lockscreenVisibility);
+ return b;
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/app/NotificationManager.java b/maven/android-compat/src/main/java/android/app/NotificationManager.java
new file mode 100644
index 00000000000..37a9a7f5855
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/app/NotificationManager.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.app;
+
+import com.codename1.ui.Display;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/// Posts notifications as Codename One local notifications, delivered now.
+///
+/// A notification whose id belongs to a running foreground service updates
+/// that service's notification instead, which is how Android code reports
+/// progress from a service (`notify(FOREGROUND_ID, builder.build())`).
+public class NotificationManager {
+
+ public static final int IMPORTANCE_UNSPECIFIED = -1000;
+ public static final int IMPORTANCE_NONE = 0;
+ public static final int IMPORTANCE_MIN = 1;
+ public static final int IMPORTANCE_LOW = 2;
+ public static final int IMPORTANCE_DEFAULT = 3;
+ public static final int IMPORTANCE_HIGH = 4;
+ public static final int IMPORTANCE_MAX = 5;
+
+ public static final String ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED =
+ "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED";
+
+ private static NotificationManager instance;
+
+ private final Map channels = new LinkedHashMap();
+
+ public static NotificationManager getInstance() {
+ if (instance == null) {
+ instance = new NotificationManager();
+ }
+ return instance;
+ }
+
+ NotificationManager() {
+ }
+
+ public void createNotificationChannel(NotificationChannel channel) {
+ channels.put(channel.getId(), channel);
+ if (Display.isInitialized()) {
+ Display.getInstance().registerNotificationChannel(channel.toBuilder());
+ }
+ }
+
+ public void createNotificationChannels(List list) {
+ for (int i = 0; i < list.size(); i++) {
+ createNotificationChannel(list.get(i));
+ }
+ }
+
+ public NotificationChannel getNotificationChannel(String channelId) {
+ return channels.get(channelId);
+ }
+
+ public List getNotificationChannels() {
+ return new ArrayList(channels.values());
+ }
+
+ public void deleteNotificationChannel(String channelId) {
+ channels.remove(channelId);
+ }
+
+ public boolean areNotificationsEnabled() {
+ return true;
+ }
+
+ public int getImportance() {
+ return IMPORTANCE_DEFAULT;
+ }
+
+ public void notify(int id, Notification notification) {
+ notify(null, id, notification);
+ }
+
+ public void notify(String tag, int id, Notification notification) {
+ if (tag == null && ActiveServices.updateForegroundNotification(id, notification)) {
+ return;
+ }
+ if (Display.isInitialized()) {
+ Display.getInstance().scheduleLocalNotification(notification.toLocal(key(tag, id)),
+ System.currentTimeMillis() + 1,
+ com.codename1.notifications.LocalNotification.REPEAT_NONE);
+ }
+ }
+
+ public void cancel(int id) {
+ cancel(null, id);
+ }
+
+ public void cancel(String tag, int id) {
+ if (Display.isInitialized()) {
+ Display.getInstance().cancelLocalNotification(key(tag, id));
+ }
+ }
+
+ /// Codename One cancels local notifications one id at a time and does not
+ /// list the ones already shown, so this cancels nothing it did not post
+ /// in this run.
+ public void cancelAll() {
+ for (int i = 0; i < posted.size(); i++) {
+ if (Display.isInitialized()) {
+ Display.getInstance().cancelLocalNotification(posted.get(i));
+ }
+ }
+ posted.clear();
+ }
+
+ private final List posted = new ArrayList();
+
+ private String key(String tag, int id) {
+ String k = tag == null ? "cn1compat:" + id : "cn1compat:" + tag + ":" + id;
+ if (!posted.contains(k)) {
+ posted.add(k);
+ }
+ return k;
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/app/PendingIntent.java b/maven/android-compat/src/main/java/android/app/PendingIntent.java
new file mode 100644
index 00000000000..6691548bded
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/app/PendingIntent.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.app;
+
+import android.content.Context;
+import android.content.Intent;
+
+/// An intent to run later, on behalf of this application. There is no other
+/// application to hand one to here, so a pending intent is the intent and the
+/// call that will start it; [#send()] makes that call. Notifications keep the
+/// one they are given (tapping a Codename One local notification brings the
+/// application forward, it does not run the intent).
+public final class PendingIntent {
+
+ public static final int FLAG_ONE_SHOT = 1 << 30;
+ public static final int FLAG_NO_CREATE = 1 << 29;
+ public static final int FLAG_CANCEL_CURRENT = 1 << 28;
+ public static final int FLAG_UPDATE_CURRENT = 1 << 27;
+ public static final int FLAG_IMMUTABLE = 1 << 26;
+ public static final int FLAG_MUTABLE = 1 << 25;
+ public static final int FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT = 1 << 24;
+
+ static final int KIND_ACTIVITY = 0;
+ static final int KIND_SERVICE = 1;
+ static final int KIND_FOREGROUND_SERVICE = 2;
+ static final int KIND_BROADCAST = 3;
+
+ /// Thrown by [#send()] once a one-shot or cancelled intent has been used.
+ public static class CanceledException extends Exception {
+ public CanceledException() {
+ }
+
+ public CanceledException(String name) {
+ super(name);
+ }
+ }
+
+ private final Context context;
+ private final Intent intent;
+ private final int kind;
+ private final int requestCode;
+ private final int flags;
+ private boolean cancelled;
+
+ private PendingIntent(Context context, int requestCode, Intent intent, int flags, int kind) {
+ this.context = context;
+ this.requestCode = requestCode;
+ this.intent = intent == null ? new Intent() : new Intent(intent);
+ this.flags = flags;
+ this.kind = kind;
+ }
+
+ public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags) {
+ return new PendingIntent(context, requestCode, intent, flags, KIND_ACTIVITY);
+ }
+
+ public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags,
+ android.os.Bundle options) {
+ return getActivity(context, requestCode, intent, flags);
+ }
+
+ public static PendingIntent getService(Context context, int requestCode, Intent intent, int flags) {
+ return new PendingIntent(context, requestCode, intent, flags, KIND_SERVICE);
+ }
+
+ public static PendingIntent getForegroundService(Context context, int requestCode, Intent intent, int flags) {
+ return new PendingIntent(context, requestCode, intent, flags, KIND_FOREGROUND_SERVICE);
+ }
+
+ public static PendingIntent getBroadcast(Context context, int requestCode, Intent intent, int flags) {
+ return new PendingIntent(context, requestCode, intent, flags, KIND_BROADCAST);
+ }
+
+ public void send() throws CanceledException {
+ if (cancelled) {
+ throw new CanceledException();
+ }
+ if ((flags & FLAG_ONE_SHOT) != 0) {
+ cancelled = true;
+ }
+ switch (kind) {
+ case KIND_ACTIVITY:
+ context.startActivity(intent);
+ break;
+ case KIND_SERVICE:
+ context.startService(intent);
+ break;
+ case KIND_FOREGROUND_SERVICE:
+ context.startForegroundService(intent);
+ break;
+ default:
+ context.sendBroadcast(intent);
+ break;
+ }
+ }
+
+ public void send(int code) throws CanceledException {
+ send();
+ }
+
+ public void cancel() {
+ cancelled = true;
+ }
+
+ public String getCreatorPackage() {
+ return context.getPackageName();
+ }
+
+ public boolean isActivity() {
+ return kind == KIND_ACTIVITY;
+ }
+
+ public boolean isService() {
+ return kind == KIND_SERVICE || kind == KIND_FOREGROUND_SERVICE;
+ }
+
+ public boolean isBroadcast() {
+ return kind == KIND_BROADCAST;
+ }
+
+ public boolean isImmutable() {
+ return (flags & FLAG_IMMUTABLE) != 0;
+ }
+
+ int requestCode() {
+ return requestCode;
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/app/Service.java b/maven/android-compat/src/main/java/android/app/Service.java
new file mode 100644
index 00000000000..53cbc7103be
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/app/Service.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.app;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.IBinder;
+
+/// An Android service, run in this process by [ActiveServices].
+///
+/// The lifecycle is Android's: `onCreate`, then `onStartCommand` for every
+/// `startService`, `onBind`/`onUnbind` for bindings, and `onDestroy` once the
+/// service is neither started nor bound. Every callback runs on the main
+/// thread (Codename One's EDT), so long work belongs on a thread of the
+/// service's own -- exactly as on Android.
+///
+/// [#startForeground(int, Notification)] runs the service under a Codename
+/// One `ForegroundService`: a persistent notification on Android, a
+/// background execution window and a notification on iOS. The service object
+/// itself does not need the platform to restart it; `START_STICKY` and the
+/// other return values are accepted and have no effect, since nothing kills a
+/// service separately from the application here.
+public abstract class Service extends ContextWrapper {
+
+ public static final int START_STICKY_COMPATIBILITY = 0;
+ public static final int START_STICKY = 1;
+ public static final int START_NOT_STICKY = 2;
+ public static final int START_REDELIVER_INTENT = 3;
+ public static final int START_FLAG_REDELIVERY = 1;
+ public static final int START_FLAG_RETRY = 2;
+ public static final int STOP_FOREGROUND_REMOVE = 1;
+ public static final int STOP_FOREGROUND_DETACH = 2;
+ public static final int STOP_FOREGROUND_LEGACY = 0;
+
+ private Application application;
+ // Read by stopSelfResult on the service's worker thread.
+ private final java.util.concurrent.atomic.AtomicInteger lastStartId =
+ new java.util.concurrent.atomic.AtomicInteger();
+
+ public Service() {
+ super(null);
+ }
+
+ final void attachForRuntime(Context base, Application app) {
+ attachBaseContext(base);
+ application = app;
+ }
+
+ public final Application getApplication() {
+ return application;
+ }
+
+ public void onCreate() {
+ }
+
+ /// Deprecated on Android; still called by the default `onStartCommand`.
+ public void onStart(Intent intent, int startId) {
+ }
+
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ onStart(intent, startId);
+ return START_STICKY;
+ }
+
+ public abstract IBinder onBind(Intent intent);
+
+ public boolean onUnbind(Intent intent) {
+ return false;
+ }
+
+ public void onRebind(Intent intent) {
+ }
+
+ public void onDestroy() {
+ }
+
+ public void onTaskRemoved(Intent rootIntent) {
+ }
+
+ public void onTimeout(int startId) {
+ }
+
+ public void onTimeout(int startId, int fgsType) {
+ }
+
+ public void onConfigurationChanged(android.content.res.Configuration newConfig) {
+ }
+
+ public void onLowMemory() {
+ }
+
+ public void onTrimMemory(int level) {
+ }
+
+ public final void stopSelf() {
+ ActiveServices.stopSelf(this, -1);
+ }
+
+ public final void stopSelf(int startId) {
+ stopSelfResult(startId);
+ }
+
+ /// Stops the service when `startId` is the most recent start, as
+ /// Android does, so a start that arrived meanwhile keeps it running.
+ public final boolean stopSelfResult(int startId) {
+ return ActiveServices.stopSelf(this, startId);
+ }
+
+ public final void startForeground(int id, Notification notification) {
+ ActiveServices.startForeground(this, id, notification);
+ }
+
+ public final void startForeground(int id, Notification notification, int foregroundServiceType) {
+ startForeground(id, notification);
+ }
+
+ public final void stopForeground(boolean removeNotification) {
+ ActiveServices.stopForeground(this);
+ }
+
+ public final void stopForeground(int notificationBehavior) {
+ ActiveServices.stopForeground(this);
+ }
+
+ public final int getForegroundServiceType() {
+ return 0;
+ }
+
+ int nextStartId() {
+ return lastStartId.incrementAndGet();
+ }
+
+ int lastStartId() {
+ return lastStartId.get();
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/content/Context.java b/maven/android-compat/src/main/java/android/content/Context.java
index 3cdb6585396..9be566d7723 100644
--- a/maven/android-compat/src/main/java/android/content/Context.java
+++ b/maven/android-compat/src/main/java/android/content/Context.java
@@ -150,6 +150,15 @@ public String getSystemServiceName(Class> serviceClass) {
if (serviceClass == android.view.inputmethod.InputMethodManager.class) {
return INPUT_METHOD_SERVICE;
}
+ if (serviceClass == android.location.LocationManager.class) {
+ return LOCATION_SERVICE;
+ }
+ if (serviceClass == android.hardware.SensorManager.class) {
+ return SENSOR_SERVICE;
+ }
+ if (serviceClass == android.app.NotificationManager.class) {
+ return NOTIFICATION_SERVICE;
+ }
return null;
}
@@ -407,4 +416,34 @@ public void unregisterReceiver(BroadcastReceiver receiver) {
public boolean isRestricted() {
return false;
}
+
+ // ------------------------------------------------------------ services
+
+ /// Starts a service the manifest declares; see `android.app.ActiveServices`.
+ public ComponentName startService(Intent service) {
+ return android.app.ActiveServices.startService(this, service);
+ }
+
+ /// The same start as [#startService(Intent)]: the service is expected to
+ /// call `startForeground`, which is what puts it in the foreground.
+ public ComponentName startForegroundService(Intent service) {
+ return android.app.ActiveServices.startService(this, service);
+ }
+
+ public boolean stopService(Intent service) {
+ return android.app.ActiveServices.stopService(this, service);
+ }
+
+ public boolean bindService(Intent service, ServiceConnection conn, int flags) {
+ return android.app.ActiveServices.bindService(this, service, conn, flags);
+ }
+
+ public boolean bindService(Intent service, int flags, java.util.concurrent.Executor executor,
+ ServiceConnection conn) {
+ return bindService(service, conn, flags);
+ }
+
+ public void unbindService(ServiceConnection conn) {
+ android.app.ActiveServices.unbindService(this, conn);
+ }
}
diff --git a/maven/android-compat/src/main/java/android/content/ServiceConnection.java b/maven/android-compat/src/main/java/android/content/ServiceConnection.java
new file mode 100644
index 00000000000..f336df233f7
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/content/ServiceConnection.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.content;
+
+import android.os.IBinder;
+
+public interface ServiceConnection {
+
+ void onServiceConnected(ComponentName name, IBinder service);
+
+ void onServiceDisconnected(ComponentName name);
+
+ default void onBindingDied(ComponentName name) {
+ }
+
+ default void onNullBinding(ComponentName name) {
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/content/pm/ServiceInfo.java b/maven/android-compat/src/main/java/android/content/pm/ServiceInfo.java
new file mode 100644
index 00000000000..5275b3ee241
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/content/pm/ServiceInfo.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.content.pm;
+
+/// The foreground service types `Service.startForeground` takes. They decide
+/// nothing here: the Codename One foreground service is the same whatever the
+/// type.
+public class ServiceInfo {
+
+ public static final int FOREGROUND_SERVICE_TYPE_NONE = 0;
+ public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1;
+ public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2;
+ public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4;
+ public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8;
+ public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16;
+ public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 32;
+ public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64;
+ public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128;
+ public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256;
+ public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 512;
+ public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1024;
+ public static final int FOREGROUND_SERVICE_TYPE_SHORT_SERVICE = 2048;
+ public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 0x40000000;
+ public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1;
+
+ public String name;
+ public String packageName;
+}
diff --git a/maven/android-compat/src/main/java/android/database/sqlite/SQLiteDatabase.java b/maven/android-compat/src/main/java/android/database/sqlite/SQLiteDatabase.java
index 5c9ba1f077e..2ec7e328e26 100644
--- a/maven/android-compat/src/main/java/android/database/sqlite/SQLiteDatabase.java
+++ b/maven/android-compat/src/main/java/android/database/sqlite/SQLiteDatabase.java
@@ -91,13 +91,15 @@ public interface CursorFactory {
/// The thread whose outermost transaction is open on the engine, or null.
///
/// There is one engine connection, so a second thread cannot have a
- /// transaction of its own beside it. Android would block that thread
- /// until the first one ends; this runtime adds no locks (serializing
+ /// transaction of its own beside it. Android blocks that thread until the
+ /// first one ends, and Room code relies on it: two executor threads
+ /// writing at once is ordinary. This runtime adds no locks (serializing
/// statements on the connection is the Codename One `Database`
- /// implementation's job), so it refuses with
- /// [SQLiteDatabaseLockedException] -- SQLite's own answer when a busy
- /// wait runs out -- rather than silently folding the second thread's
- /// work into the first one's transaction.
+ /// implementation's job), so the second thread busy-waits for the owner
+ /// to clear, as SQLite does under its busy timeout, and is refused with
+ /// [SQLiteDatabaseLockedException] -- SQLite's answer when that wait runs
+ /// out -- only after [#BUSY_TIMEOUT_MS]. Its work is never folded into the
+ /// first thread's transaction.
///
/// An atomic rather than a lock: the outermost `beginTransaction` claims
/// it with `compareAndSet(null, current)` before the engine's BEGIN, so
@@ -117,13 +119,69 @@ public interface CursorFactory {
/// no-lock reason as `beginTransaction` this refuses it with the same
/// [SQLiteDatabaseLockedException].
private void checkNotInOtherThreadsTransaction(String sql) {
- Thread owner = mTransactionOwner.get();
- if (owner != null && owner != Thread.currentThread()) {
+ if (!awaitNoForeignOwner()) {
throw new SQLiteDatabaseLockedException("database is locked: another thread's transaction is open, "
+ "while executing: " + sql);
}
}
+ /// How long a thread waits for another thread's transaction to end, in
+ /// milliseconds; tests shorten it to observe the refusal.
+ static final java.util.concurrent.atomic.AtomicLong BUSY_TIMEOUT_MS =
+ new java.util.concurrent.atomic.AtomicLong(10000);
+
+ /// How long the main thread waits, in milliseconds. Shorter: a worker's
+ /// transaction may itself be waiting on the main thread (a posted
+ /// callback, a `Future` of main thread work), and the UI is frozen for
+ /// as long as this waits.
+ static final java.util.concurrent.atomic.AtomicLong MAIN_BUSY_TIMEOUT_MS =
+ new java.util.concurrent.atomic.AtomicLong(2000);
+
+ private static long deadline() {
+ if (com.codename1.androidcompat.runtime.MainThread.isMainThread()
+ && com.codename1.ui.Display.isInitialized()) {
+ return System.currentTimeMillis() + MAIN_BUSY_TIMEOUT_MS.get();
+ }
+ return System.currentTimeMillis() + BUSY_TIMEOUT_MS.get();
+ }
+
+ private boolean awaitNoForeignOwner() {
+ Thread current = Thread.currentThread();
+ long deadline = deadline();
+ while (true) {
+ Thread owner = mTransactionOwner.get();
+ if (owner == null || owner == current) {
+ return true;
+ }
+ if (!pause(deadline)) {
+ return false;
+ }
+ }
+ }
+
+ private boolean claimOwner(Thread current) {
+ long deadline = deadline();
+ while (!mTransactionOwner.compareAndSet(null, current)) {
+ if (!pause(deadline)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean pause(long deadline) {
+ if (System.currentTimeMillis() >= deadline) {
+ return false;
+ }
+ try {
+ Thread.sleep(2);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ return true;
+ }
+
private static final class Transaction {
boolean markedSuccessful;
boolean childFailed;
@@ -387,7 +445,7 @@ public void beginTransactionWithListener(SQLiteTransactionListener transactionLi
}
if (stack.isEmpty()) {
Thread current = Thread.currentThread();
- if (!mTransactionOwner.compareAndSet(null, current)) {
+ if (!claimOwner(current)) {
throw new SQLiteDatabaseLockedException("database is locked: another thread's transaction is open");
}
try {
diff --git a/maven/android-compat/src/main/java/android/hardware/Sensor.java b/maven/android-compat/src/main/java/android/hardware/Sensor.java
new file mode 100644
index 00000000000..1efa41e3b2e
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/hardware/Sensor.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.hardware;
+
+/// One sensor [SensorManager#getDefaultSensor(int)] answered. Only the types
+/// Codename One can feed exist; [SensorManager] answers null for the others,
+/// exactly as Android does on a device without that hardware.
+public final class Sensor {
+
+ public static final int TYPE_ALL = -1;
+ public static final int TYPE_ACCELEROMETER = 1;
+ public static final int TYPE_MAGNETIC_FIELD = 2;
+ /// Deprecated on Android; still delivered, in degrees.
+ public static final int TYPE_ORIENTATION = 3;
+ public static final int TYPE_GYROSCOPE = 4;
+ public static final int TYPE_LIGHT = 5;
+ public static final int TYPE_PRESSURE = 6;
+ public static final int TYPE_TEMPERATURE = 7;
+ public static final int TYPE_PROXIMITY = 8;
+ public static final int TYPE_GRAVITY = 9;
+ public static final int TYPE_LINEAR_ACCELERATION = 10;
+ public static final int TYPE_ROTATION_VECTOR = 11;
+ public static final int TYPE_RELATIVE_HUMIDITY = 12;
+ public static final int TYPE_AMBIENT_TEMPERATURE = 13;
+ public static final int TYPE_MAGNETIC_FIELD_UNCALIBRATED = 14;
+ public static final int TYPE_GAME_ROTATION_VECTOR = 15;
+ public static final int TYPE_GYROSCOPE_UNCALIBRATED = 16;
+ public static final int TYPE_SIGNIFICANT_MOTION = 17;
+ public static final int TYPE_STEP_DETECTOR = 18;
+ public static final int TYPE_STEP_COUNTER = 19;
+ public static final int TYPE_GEOMAGNETIC_ROTATION_VECTOR = 20;
+ public static final int TYPE_HEART_RATE = 21;
+ public static final int TYPE_ACCELEROMETER_UNCALIBRATED = 35;
+
+ public static final String STRING_TYPE_ACCELEROMETER = "android.sensor.accelerometer";
+ public static final String STRING_TYPE_MAGNETIC_FIELD = "android.sensor.magnetic_field";
+ public static final String STRING_TYPE_ORIENTATION = "android.sensor.orientation";
+ public static final String STRING_TYPE_GYROSCOPE = "android.sensor.gyroscope";
+ public static final String STRING_TYPE_GRAVITY = "android.sensor.gravity";
+ public static final String STRING_TYPE_LINEAR_ACCELERATION = "android.sensor.linear_acceleration";
+ public static final String STRING_TYPE_ROTATION_VECTOR = "android.sensor.rotation_vector";
+ public static final String STRING_TYPE_GEOMAGNETIC_ROTATION_VECTOR = "android.sensor.geomagnetic_rotation_vector";
+
+ public static final int REPORTING_MODE_CONTINUOUS = 0;
+ public static final int REPORTING_MODE_ON_CHANGE = 1;
+ public static final int REPORTING_MODE_ONE_SHOT = 2;
+ public static final int REPORTING_MODE_SPECIAL_TRIGGER = 3;
+
+ private final int type;
+ private final String name;
+ private final String stringType;
+ private final float maximumRange;
+ private final float resolution;
+
+ Sensor(int type, String name, String stringType, float maximumRange, float resolution) {
+ this.type = type;
+ this.name = name;
+ this.stringType = stringType;
+ this.maximumRange = maximumRange;
+ this.resolution = resolution;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getStringType() {
+ return stringType;
+ }
+
+ public String getVendor() {
+ return "Codename One";
+ }
+
+ public int getVersion() {
+ return 1;
+ }
+
+ public float getMaximumRange() {
+ return maximumRange;
+ }
+
+ public float getResolution() {
+ return resolution;
+ }
+
+ /// Unknown here; Android reports milliamperes.
+ public float getPower() {
+ return 0;
+ }
+
+ /// The fastest sampling Codename One asks the platform for, in
+ /// microseconds.
+ public int getMinDelay() {
+ return 10000;
+ }
+
+ public int getMaxDelay() {
+ return 1000000;
+ }
+
+ public int getReportingMode() {
+ return REPORTING_MODE_CONTINUOUS;
+ }
+
+ public int getFifoMaxEventCount() {
+ return 0;
+ }
+
+ public int getFifoReservedEventCount() {
+ return 0;
+ }
+
+ public boolean isWakeUpSensor() {
+ return false;
+ }
+
+ public boolean isDynamicSensor() {
+ return false;
+ }
+
+ public int getId() {
+ return type;
+ }
+
+ @Override
+ public String toString() {
+ return "{Sensor name=\"" + name + "\", vendor=\"Codename One\", type=" + type + "}";
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/hardware/SensorEvent.java b/maven/android-compat/src/main/java/android/hardware/SensorEvent.java
new file mode 100644
index 00000000000..208ccd5ee47
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/hardware/SensorEvent.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.hardware;
+
+/// One reading. As on Android the array is the listener's to read during the
+/// callback only: the next event of the same registration reuses it.
+public class SensorEvent {
+
+ public final float[] values;
+ public Sensor sensor;
+ public int accuracy;
+ /// Nanoseconds on the `SystemClock.elapsedRealtimeNanos()` time base.
+ public long timestamp;
+
+ SensorEvent(int valueSize) {
+ values = new float[valueSize];
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/hardware/SensorEventListener.java b/maven/android-compat/src/main/java/android/hardware/SensorEventListener.java
new file mode 100644
index 00000000000..8790d6ce52a
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/hardware/SensorEventListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.hardware;
+
+public interface SensorEventListener {
+
+ void onSensorChanged(SensorEvent event);
+
+ void onAccuracyChanged(Sensor sensor, int accuracy);
+}
diff --git a/maven/android-compat/src/main/java/android/hardware/SensorManager.java b/maven/android-compat/src/main/java/android/hardware/SensorManager.java
new file mode 100644
index 00000000000..9bd0115e7d2
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/hardware/SensorManager.java
@@ -0,0 +1,591 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.hardware;
+
+import android.os.Handler;
+import com.codename1.sensors.MotionEvent;
+import com.codename1.sensors.MotionSensor;
+import com.codename1.sensors.MotionSensorListener;
+import com.codename1.sensors.MotionSensorManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/// Android's sensor service over `com.codename1.sensors.MotionSensorManager`.
+///
+/// The raw sensors map one to one and in the same units (m/s^2, rad/s,
+/// microtesla): accelerometer, gyroscope, magnetic field, and Codename One's
+/// gravity and linear acceleration. The fused ones Android computes from them
+/// are computed here from Codename One's gravity and magnetometer with the
+/// same matrix math [#getRotationMatrix] performs:
+///
+/// - `TYPE_ROTATION_VECTOR` and `TYPE_GEOMAGNETIC_ROTATION_VECTOR`, the unit
+/// quaternion of that matrix (x, y, z, w, and -1 for the unknown heading
+/// accuracy);
+/// - the deprecated `TYPE_ORIENTATION`, in degrees, azimuth in [0, 360).
+///
+/// Both reference magnetic north, as Android's do; add
+/// `GeomagneticField.getDeclination()` for true north. Android also fuses the
+/// gyroscope into its rotation vector, so this one is noisier between two
+/// magnetometer readings -- low-pass the azimuth for a compass needle.
+///
+/// A type Codename One cannot feed (light, pressure, proximity, step counter,
+/// game rotation vector) is absent: [#getDefaultSensor(int)] answers null for
+/// it, which Android code already handles for a device without the hardware.
+///
+/// Readings arrive on the main thread (Codename One's EDT), or on the looper
+/// of the `Handler` a registration names. The sampling period applies to every
+/// sensor at once, because Codename One samples on one interval: the shortest
+/// period any registration asked for.
+public class SensorManager {
+
+ public static final int SENSOR_DELAY_FASTEST = 0;
+ public static final int SENSOR_DELAY_GAME = 1;
+ public static final int SENSOR_DELAY_UI = 2;
+ public static final int SENSOR_DELAY_NORMAL = 3;
+
+ public static final int SENSOR_STATUS_NO_CONTACT = -1;
+ public static final int SENSOR_STATUS_UNRELIABLE = 0;
+ public static final int SENSOR_STATUS_ACCURACY_LOW = 1;
+ public static final int SENSOR_STATUS_ACCURACY_MEDIUM = 2;
+ public static final int SENSOR_STATUS_ACCURACY_HIGH = 3;
+
+ public static final float STANDARD_GRAVITY = 9.80665f;
+ public static final float GRAVITY_EARTH = 9.80665f;
+ public static final float MAGNETIC_FIELD_EARTH_MAX = 60.0f;
+ public static final float MAGNETIC_FIELD_EARTH_MIN = 30.0f;
+ public static final float PRESSURE_STANDARD_ATMOSPHERE = 1013.25f;
+
+ public static final int AXIS_X = 1;
+ public static final int AXIS_Y = 2;
+ public static final int AXIS_Z = 3;
+ public static final int AXIS_MINUS_X = AXIS_X | 0x80;
+ public static final int AXIS_MINUS_Y = AXIS_Y | 0x80;
+ public static final int AXIS_MINUS_Z = AXIS_Z | 0x80;
+
+ /// The Codename One manager to use instead of the platform's; set by tests.
+ static MotionSensorManager override;
+
+ private static SensorManager instance;
+
+ private final Source[] sources = new Source[7];
+ private final List registrations = new ArrayList();
+ private final float[] matrix = new float[9];
+ private final float[] scratch = new float[3];
+
+ public static SensorManager getInstance() {
+ if (instance == null) {
+ instance = new SensorManager();
+ }
+ return instance;
+ }
+
+ SensorManager() {
+ }
+
+ static MotionSensorManager platform() {
+ return override != null ? override : MotionSensorManager.getInstance();
+ }
+
+ // -------------------------------------------------------------- sensors
+
+ /// The Codename One sensor types an Android type is computed from.
+ static int[] sourcesOf(int type) {
+ switch (type) {
+ case Sensor.TYPE_ACCELEROMETER:
+ return new int[] {MotionSensorManager.TYPE_ACCELEROMETER};
+ case Sensor.TYPE_MAGNETIC_FIELD:
+ return new int[] {MotionSensorManager.TYPE_MAGNETOMETER};
+ case Sensor.TYPE_GYROSCOPE:
+ return new int[] {MotionSensorManager.TYPE_GYROSCOPE};
+ case Sensor.TYPE_GRAVITY:
+ return new int[] {MotionSensorManager.TYPE_GRAVITY};
+ case Sensor.TYPE_LINEAR_ACCELERATION:
+ return new int[] {MotionSensorManager.TYPE_LINEAR_ACCELERATION};
+ case Sensor.TYPE_ROTATION_VECTOR:
+ case Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR:
+ case Sensor.TYPE_ORIENTATION:
+ return new int[] {MotionSensorManager.TYPE_GRAVITY, MotionSensorManager.TYPE_MAGNETOMETER};
+ default:
+ return null;
+ }
+ }
+
+ private static Sensor describe(int type) {
+ switch (type) {
+ case Sensor.TYPE_ACCELEROMETER:
+ return new Sensor(type, "Accelerometer", Sensor.STRING_TYPE_ACCELEROMETER, 4 * GRAVITY_EARTH, 0.01f);
+ case Sensor.TYPE_MAGNETIC_FIELD:
+ return new Sensor(type, "Magnetic field", Sensor.STRING_TYPE_MAGNETIC_FIELD, 2000f, 0.1f);
+ case Sensor.TYPE_GYROSCOPE:
+ return new Sensor(type, "Gyroscope", Sensor.STRING_TYPE_GYROSCOPE, 35f, 0.001f);
+ case Sensor.TYPE_GRAVITY:
+ return new Sensor(type, "Gravity", Sensor.STRING_TYPE_GRAVITY, GRAVITY_EARTH, 0.01f);
+ case Sensor.TYPE_LINEAR_ACCELERATION:
+ return new Sensor(type, "Linear acceleration", Sensor.STRING_TYPE_LINEAR_ACCELERATION,
+ 4 * GRAVITY_EARTH, 0.01f);
+ case Sensor.TYPE_ROTATION_VECTOR:
+ return new Sensor(type, "Rotation vector", Sensor.STRING_TYPE_ROTATION_VECTOR, 1f, 0.0001f);
+ case Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR:
+ return new Sensor(type, "Geomagnetic rotation vector",
+ Sensor.STRING_TYPE_GEOMAGNETIC_ROTATION_VECTOR, 1f, 0.0001f);
+ case Sensor.TYPE_ORIENTATION:
+ return new Sensor(type, "Orientation", Sensor.STRING_TYPE_ORIENTATION, 360f, 0.1f);
+ default:
+ return null;
+ }
+ }
+
+ private static final int[] ALL_TYPES = {
+ Sensor.TYPE_ACCELEROMETER, Sensor.TYPE_MAGNETIC_FIELD, Sensor.TYPE_ORIENTATION,
+ Sensor.TYPE_GYROSCOPE, Sensor.TYPE_GRAVITY, Sensor.TYPE_LINEAR_ACCELERATION,
+ Sensor.TYPE_ROTATION_VECTOR, Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR,
+ };
+
+ private final Sensor[] described = new Sensor[64];
+
+ public Sensor getDefaultSensor(int type) {
+ int[] needs = sourcesOf(type);
+ if (needs == null) {
+ return null;
+ }
+ MotionSensorManager m = platform();
+ for (int i = 0; i < needs.length; i++) {
+ if (!m.isSensorSupported(needs[i])) {
+ return null;
+ }
+ }
+ if (described[type] == null) {
+ described[type] = describe(type);
+ }
+ return described[type];
+ }
+
+ public Sensor getDefaultSensor(int type, boolean wakeUp) {
+ return wakeUp ? null : getDefaultSensor(type);
+ }
+
+ public List getSensorList(int type) {
+ List out = new ArrayList();
+ for (int i = 0; i < ALL_TYPES.length; i++) {
+ if (type == Sensor.TYPE_ALL || type == ALL_TYPES[i]) {
+ Sensor s = getDefaultSensor(ALL_TYPES[i]);
+ if (s != null) {
+ out.add(s);
+ }
+ }
+ }
+ return out;
+ }
+
+ public List getDynamicSensorList(int type) {
+ return new ArrayList();
+ }
+
+ public boolean isDynamicSensorDiscoverySupported() {
+ return false;
+ }
+
+ // -------------------------------------------------------- registration
+
+ public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
+ return registerListener(listener, sensor, samplingPeriodUs, (Handler) null);
+ }
+
+ public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs,
+ int maxReportLatencyUs) {
+ return registerListener(listener, sensor, samplingPeriodUs, (Handler) null);
+ }
+
+ public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs,
+ int maxReportLatencyUs, Handler handler) {
+ return registerListener(listener, sensor, samplingPeriodUs, handler);
+ }
+
+ public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs,
+ Handler handler) {
+ if (listener == null || sensor == null) {
+ return false;
+ }
+ for (int i = 0; i < registrations.size(); i++) {
+ Registration r = registrations.get(i);
+ if (r.listener == listener && r.sensor == sensor) {
+ // Android keeps the first registration of the pair.
+ return true;
+ }
+ }
+ int[] needs = sourcesOf(sensor.getType());
+ if (needs == null) {
+ return false;
+ }
+ registrations.add(new Registration(listener, sensor, periodUs(samplingPeriodUs), handler));
+ for (int i = 0; i < needs.length; i++) {
+ acquire(needs[i]);
+ }
+ retune();
+ return true;
+ }
+
+ public void unregisterListener(SensorEventListener listener) {
+ unregisterListener(listener, null);
+ }
+
+ public void unregisterListener(SensorEventListener listener, Sensor sensor) {
+ for (int i = registrations.size() - 1; i >= 0; i--) {
+ Registration r = registrations.get(i);
+ if (r.listener == listener && (sensor == null || r.sensor == sensor)) {
+ registrations.remove(i);
+ int[] needs = sourcesOf(r.sensor.getType());
+ for (int j = 0; j < needs.length; j++) {
+ release(needs[j]);
+ }
+ }
+ }
+ retune();
+ }
+
+ public boolean flush(SensorEventListener listener) {
+ return false;
+ }
+
+ /// Android's SENSOR_DELAY_* constants or a period in microseconds.
+ static int periodUs(int samplingPeriodUs) {
+ switch (samplingPeriodUs) {
+ case SENSOR_DELAY_FASTEST:
+ return 0;
+ case SENSOR_DELAY_GAME:
+ return 20000;
+ case SENSOR_DELAY_UI:
+ return 66667;
+ case SENSOR_DELAY_NORMAL:
+ return 200000;
+ default:
+ return Math.max(0, samplingPeriodUs);
+ }
+ }
+
+ private void retune() {
+ if (registrations.isEmpty()) {
+ return;
+ }
+ int min = Integer.MAX_VALUE;
+ for (int i = 0; i < registrations.size(); i++) {
+ min = Math.min(min, registrations.get(i).periodUs);
+ }
+ // Codename One samples in milliseconds; 10ms is as fast as it is asked to go.
+ platform().setSamplingInterval(Math.max(10, min / 1000));
+ }
+
+ private void acquire(int cn1Type) {
+ Source s = sources[cn1Type];
+ if (s == null) {
+ s = new Source(cn1Type);
+ sources[cn1Type] = s;
+ }
+ if (s.users++ == 0) {
+ s.start();
+ }
+ }
+
+ private void release(int cn1Type) {
+ Source s = sources[cn1Type];
+ if (s != null && s.users > 0 && --s.users == 0) {
+ s.stop();
+ }
+ }
+
+ // ------------------------------------------------------------ delivery
+
+ /// A Codename One reading arrived; every registration computed from that
+ /// sensor gets an event.
+ void feed(int cn1Type, float x, float y, float z) {
+ Source src = sources[cn1Type];
+ if (src == null) {
+ return;
+ }
+ src.values[0] = x;
+ src.values[1] = y;
+ src.values[2] = z;
+ src.has = true;
+ long now = android.os.SystemClock.elapsedRealtimeNanos();
+ Registration[] snapshot = registrations.toArray(new Registration[registrations.size()]);
+ for (int i = 0; i < snapshot.length; i++) {
+ Registration r = snapshot[i];
+ if (!registrations.contains(r)) {
+ // Unregistered by an earlier listener in this pass.
+ continue;
+ }
+ int[] needs = sourcesOf(r.sensor.getType());
+ if (!contains(needs, cn1Type)) {
+ continue;
+ }
+ if (r.lastNanos != 0 && now - r.lastNanos < r.periodUs * 1000L) {
+ continue;
+ }
+ if (!compute(r.sensor.getType(), r.event.values)) {
+ continue;
+ }
+ r.lastNanos = now;
+ r.event.timestamp = now;
+ r.deliver();
+ }
+ }
+
+ private static boolean contains(int[] a, int v) {
+ for (int i = 0; i < a.length; i++) {
+ if (a[i] == v) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// The values of one Android event, or false while a source has not
+ /// reported yet.
+ private boolean compute(int type, float[] out) {
+ switch (type) {
+ case Sensor.TYPE_ACCELEROMETER:
+ return copy(MotionSensorManager.TYPE_ACCELEROMETER, out);
+ case Sensor.TYPE_MAGNETIC_FIELD:
+ return copy(MotionSensorManager.TYPE_MAGNETOMETER, out);
+ case Sensor.TYPE_GYROSCOPE:
+ return copy(MotionSensorManager.TYPE_GYROSCOPE, out);
+ case Sensor.TYPE_GRAVITY:
+ return copy(MotionSensorManager.TYPE_GRAVITY, out);
+ case Sensor.TYPE_LINEAR_ACCELERATION:
+ return copy(MotionSensorManager.TYPE_LINEAR_ACCELERATION, out);
+ default:
+ break;
+ }
+ Source g = sources[MotionSensorManager.TYPE_GRAVITY];
+ Source m = sources[MotionSensorManager.TYPE_MAGNETOMETER];
+ if (g == null || m == null || !g.has || !m.has) {
+ return false;
+ }
+ if (!getRotationMatrix(matrix, null, g.values, m.values)) {
+ // Free fall, or next to a magnet: Android delivers nothing either.
+ return false;
+ }
+ if (type == Sensor.TYPE_ORIENTATION) {
+ getOrientation(matrix, scratch);
+ float azimuth = (float) Math.toDegrees(scratch[0]);
+ out[0] = azimuth < 0 ? azimuth + 360f : azimuth;
+ out[1] = (float) Math.toDegrees(scratch[1]);
+ out[2] = (float) Math.toDegrees(scratch[2]);
+ return true;
+ }
+ quaternionFromMatrix(matrix, out);
+ return true;
+ }
+
+ private boolean copy(int cn1Type, float[] out) {
+ Source s = sources[cn1Type];
+ if (s == null || !s.has) {
+ return false;
+ }
+ out[0] = s.values[0];
+ out[1] = s.values[1];
+ out[2] = s.values[2];
+ return true;
+ }
+
+ /// The rotation vector of a device-to-world rotation matrix: the unit
+ /// quaternion's x, y, z, its w (never negative, as Android's), and -1
+ /// for the heading accuracy nobody estimated. Shepperd's method, so it
+ /// stays accurate near a half turn.
+ static void quaternionFromMatrix(float[] r, float[] out) {
+ double m00 = r[0], m01 = r[1], m02 = r[2];
+ double m10 = r[3], m11 = r[4], m12 = r[5];
+ double m20 = r[6], m21 = r[7], m22 = r[8];
+ double trace = m00 + m11 + m22;
+ double w, x, y, z;
+ if (trace > 0) {
+ double s = Math.sqrt(trace + 1.0) * 2;
+ w = 0.25 * s;
+ x = (m21 - m12) / s;
+ y = (m02 - m20) / s;
+ z = (m10 - m01) / s;
+ } else if (m00 > m11 && m00 > m22) {
+ double s = Math.sqrt(1.0 + m00 - m11 - m22) * 2;
+ w = (m21 - m12) / s;
+ x = 0.25 * s;
+ y = (m01 + m10) / s;
+ z = (m02 + m20) / s;
+ } else if (m11 > m22) {
+ double s = Math.sqrt(1.0 + m11 - m00 - m22) * 2;
+ w = (m02 - m20) / s;
+ x = (m01 + m10) / s;
+ y = 0.25 * s;
+ z = (m12 + m21) / s;
+ } else {
+ double s = Math.sqrt(1.0 + m22 - m00 - m11) * 2;
+ w = (m10 - m01) / s;
+ x = (m02 + m20) / s;
+ y = (m12 + m21) / s;
+ z = 0.25 * s;
+ }
+ if (w < 0) {
+ w = -w;
+ x = -x;
+ y = -y;
+ z = -z;
+ }
+ out[0] = (float) x;
+ out[1] = (float) y;
+ out[2] = (float) z;
+ if (out.length > 3) {
+ out[3] = (float) w;
+ }
+ if (out.length > 4) {
+ out[4] = -1f;
+ }
+ }
+
+ // ----------------------------------------------------------- static math
+
+ /// Android's rotation and inclination matrices from a gravity and a
+ /// geomagnetic vector, both in device coordinates. `R` and `I` may be
+ /// 9 (3x3) or 16 (4x4) long, row major; either may be null. Answers false
+ /// in free fall or when the field is parallel to gravity.
+ public static boolean getRotationMatrix(float[] R, float[] I, float[] gravity, float[] geomagnetic) {
+ return SensorMath.getRotationMatrix(R, I, gravity, geomagnetic);
+ }
+
+ /// The magnetic dip angle, in radians, of an inclination matrix.
+ public static float getInclination(float[] I) {
+ return SensorMath.getInclination(I);
+ }
+
+ /// Azimuth, pitch and roll in radians from a rotation matrix: azimuth in
+ /// [-pi, pi], 0 at magnetic north.
+ public static float[] getOrientation(float[] R, float[] values) {
+ return SensorMath.getOrientation(R, values);
+ }
+
+ /// The rotation matrix of a rotation vector (x, y, z[, w]).
+ public static void getRotationMatrixFromVector(float[] R, float[] rotationVector) {
+ SensorMath.getRotationMatrixFromVector(R, rotationVector);
+ }
+
+ /// The unit quaternion (w, x, y, z) of a rotation vector.
+ public static void getQuaternionFromVector(float[] Q, float[] rv) {
+ SensorMath.getQuaternionFromVector(Q, rv);
+ }
+
+ /// Re-expresses `inR` for a device whose `X` and `Y` axes lie along the
+ /// given world axes (the AXIS_* constants).
+ public static boolean remapCoordinateSystem(float[] inR, int X, int Y, float[] outR) {
+ return SensorMath.remapCoordinateSystem(inR, X, Y, outR);
+ }
+
+ /// The angle change between two rotation matrices.
+ public static void getAngleChange(float[] angleChange, float[] R, float[] prevR) {
+ SensorMath.getAngleChange(angleChange, R, prevR);
+ }
+
+ /// Altitude in meters from the pressure at sea level and here, in hPa.
+ public static float getAltitude(float p0, float p) {
+ return SensorMath.getAltitude(p0, p);
+ }
+
+ // ------------------------------------------------------------- helpers
+
+ /// One Codename One sensor, listened to while any registration needs it.
+ final class Source {
+ final int type;
+ final float[] values = new float[3];
+ boolean has;
+ int users;
+ private MotionSensor sensor;
+ private final MotionSensorListener listener = new MotionSensorListener() {
+ @Override
+ public void motionReceived(MotionEvent evt) {
+ feed(type, evt.getX(), evt.getY(), evt.getZ());
+ }
+ };
+
+ Source(int type) {
+ this.type = type;
+ }
+
+ void start() {
+ has = false;
+ if (override == null || sensor == null) {
+ sensor = platform().getSensor(type);
+ }
+ if (sensor != null) {
+ sensor.addListener(listener);
+ }
+ }
+
+ void stop() {
+ if (sensor != null) {
+ sensor.removeListener(listener);
+ }
+ has = false;
+ }
+ }
+
+ final class Registration {
+ final SensorEventListener listener;
+ final Sensor sensor;
+ final int periodUs;
+ final Handler handler;
+ final SensorEvent event;
+ long lastNanos;
+ private boolean accuracyReported;
+
+ Registration(SensorEventListener listener, Sensor sensor, int periodUs, Handler handler) {
+ this.listener = listener;
+ this.sensor = sensor;
+ this.periodUs = periodUs;
+ this.handler = handler;
+ int size = sensor.getType() == Sensor.TYPE_ROTATION_VECTOR
+ || sensor.getType() == Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR ? 5 : 3;
+ event = new SensorEvent(size);
+ event.sensor = sensor;
+ event.accuracy = SENSOR_STATUS_ACCURACY_HIGH;
+ }
+
+ void deliver() {
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ if (!registrations.contains(Registration.this)) {
+ return;
+ }
+ if (!accuracyReported) {
+ accuracyReported = true;
+ listener.onAccuracyChanged(sensor, event.accuracy);
+ }
+ listener.onSensorChanged(event);
+ }
+ };
+ if (handler != null && !handler.getLooper().isCurrentThread()) {
+ handler.post(r);
+ } else {
+ r.run();
+ }
+ }
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/hardware/SensorMath.java b/maven/android-compat/src/main/java/android/hardware/SensorMath.java
new file mode 100644
index 00000000000..cef24ca0599
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/hardware/SensorMath.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2008 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.
+ *
+ * Modifications copyright (c) 2026, Codename One and/or its affiliates:
+ * rewritten from the static methods of android.hardware.SensorManager for the
+ * Codename One compatibility runtime (com.codename1.util.MathUtil in place of
+ * the trigonometry CLDC lacks).
+ */
+package android.hardware;
+
+import com.codename1.util.MathUtil;
+
+/// The sensor math of Android's `SensorManager`, so a compass computed with
+/// `getRotationMatrix` and `getOrientation` reads the same numbers here as on
+/// Android. [SensorManager] exposes it under Android's names.
+final class SensorMath {
+
+ private SensorMath() {
+ }
+
+
+ /// Android's rotation and inclination matrices from a gravity and a
+ /// geomagnetic vector, both in device coordinates. `R` and `I` may be
+ /// 9 (3x3) or 16 (4x4) long, row major; either may be null. Answers false
+ /// in free fall or when the field is parallel to gravity.
+ static boolean getRotationMatrix(float[] R, float[] I, float[] gravity, float[] geomagnetic) {
+ float ax = gravity[0], ay = gravity[1], az = gravity[2];
+ final float normsqA = ax * ax + ay * ay + az * az;
+ final float g = 9.81f;
+ final float freeFallGravitySquared = 0.01f * g * g;
+ if (normsqA < freeFallGravitySquared) {
+ return false;
+ }
+ final float ex = geomagnetic[0], ey = geomagnetic[1], ez = geomagnetic[2];
+ float hx = ey * az - ez * ay;
+ float hy = ez * ax - ex * az;
+ float hz = ex * ay - ey * ax;
+ final float normH = (float) Math.sqrt(hx * hx + hy * hy + hz * hz);
+ if (normH < 0.1f) {
+ return false;
+ }
+ final float invH = 1.0f / normH;
+ hx *= invH;
+ hy *= invH;
+ hz *= invH;
+ final float invA = 1.0f / (float) Math.sqrt(normsqA);
+ ax *= invA;
+ ay *= invA;
+ az *= invA;
+ final float mx = ay * hz - az * hy;
+ final float my = az * hx - ax * hz;
+ final float mz = ax * hy - ay * hx;
+ if (R != null) {
+ if (R.length == 9) {
+ R[0] = hx; R[1] = hy; R[2] = hz;
+ R[3] = mx; R[4] = my; R[5] = mz;
+ R[6] = ax; R[7] = ay; R[8] = az;
+ } else if (R.length == 16) {
+ R[0] = hx; R[1] = hy; R[2] = hz; R[3] = 0;
+ R[4] = mx; R[5] = my; R[6] = mz; R[7] = 0;
+ R[8] = ax; R[9] = ay; R[10] = az; R[11] = 0;
+ R[12] = 0; R[13] = 0; R[14] = 0; R[15] = 1;
+ }
+ }
+ if (I != null) {
+ final float invE = 1.0f / (float) Math.sqrt(ex * ex + ey * ey + ez * ez);
+ final float c = (ex * mx + ey * my + ez * mz) * invE;
+ final float s = (ex * ax + ey * ay + ez * az) * invE;
+ if (I.length == 9) {
+ I[0] = 1; I[1] = 0; I[2] = 0;
+ I[3] = 0; I[4] = c; I[5] = s;
+ I[6] = 0; I[7] = -s; I[8] = c;
+ } else if (I.length == 16) {
+ I[0] = 1; I[1] = 0; I[2] = 0; I[3] = 0;
+ I[4] = 0; I[5] = c; I[6] = s; I[7] = 0;
+ I[8] = 0; I[9] = -s; I[10] = c; I[11] = 0;
+ I[12] = 0; I[13] = 0; I[14] = 0; I[15] = 1;
+ }
+ }
+ return true;
+ }
+
+ /// The magnetic dip angle, in radians, of an inclination matrix.
+ static float getInclination(float[] I) {
+ if (I.length == 9) {
+ return (float) MathUtil.atan2(I[5], I[4]);
+ }
+ return (float) MathUtil.atan2(I[6], I[5]);
+ }
+
+ /// Azimuth, pitch and roll in radians from a rotation matrix, as Android
+ /// computes them: azimuth in [-pi, pi], 0 at magnetic north.
+ static float[] getOrientation(float[] R, float[] values) {
+ if (R.length == 9) {
+ values[0] = (float) MathUtil.atan2(R[1], R[4]);
+ values[1] = (float) MathUtil.asin(-R[7]);
+ values[2] = (float) MathUtil.atan2(-R[6], R[8]);
+ } else {
+ values[0] = (float) MathUtil.atan2(R[1], R[5]);
+ values[1] = (float) MathUtil.asin(-R[9]);
+ values[2] = (float) MathUtil.atan2(-R[8], R[10]);
+ }
+ return values;
+ }
+
+ /// The rotation matrix of a rotation vector (x, y, z[, w]); w is derived
+ /// when the vector has only three components.
+ static void getRotationMatrixFromVector(float[] R, float[] rotationVector) {
+ float q0;
+ float q1 = rotationVector[0];
+ float q2 = rotationVector[1];
+ float q3 = rotationVector[2];
+ if (rotationVector.length >= 4) {
+ q0 = rotationVector[3];
+ } else {
+ q0 = 1 - q1 * q1 - q2 * q2 - q3 * q3;
+ q0 = q0 > 0 ? (float) Math.sqrt(q0) : 0;
+ }
+ float sqq1 = 2 * q1 * q1;
+ float sqq2 = 2 * q2 * q2;
+ float sqq3 = 2 * q3 * q3;
+ float q1q2 = 2 * q1 * q2;
+ float q3q0 = 2 * q3 * q0;
+ float q1q3 = 2 * q1 * q3;
+ float q2q0 = 2 * q2 * q0;
+ float q2q3 = 2 * q2 * q3;
+ float q1q0 = 2 * q1 * q0;
+ if (R.length == 9) {
+ R[0] = 1 - sqq2 - sqq3; R[1] = q1q2 - q3q0; R[2] = q1q3 + q2q0;
+ R[3] = q1q2 + q3q0; R[4] = 1 - sqq1 - sqq3; R[5] = q2q3 - q1q0;
+ R[6] = q1q3 - q2q0; R[7] = q2q3 + q1q0; R[8] = 1 - sqq1 - sqq2;
+ } else if (R.length == 16) {
+ R[0] = 1 - sqq2 - sqq3; R[1] = q1q2 - q3q0; R[2] = q1q3 + q2q0; R[3] = 0;
+ R[4] = q1q2 + q3q0; R[5] = 1 - sqq1 - sqq3; R[6] = q2q3 - q1q0; R[7] = 0;
+ R[8] = q1q3 - q2q0; R[9] = q2q3 + q1q0; R[10] = 1 - sqq1 - sqq2; R[11] = 0;
+ R[12] = 0; R[13] = 0; R[14] = 0; R[15] = 1;
+ }
+ }
+
+ /// The unit quaternion (w, x, y, z) of a rotation vector.
+ static void getQuaternionFromVector(float[] Q, float[] rv) {
+ if (rv.length >= 4) {
+ Q[0] = rv[3];
+ } else {
+ float w = 1 - rv[0] * rv[0] - rv[1] * rv[1] - rv[2] * rv[2];
+ Q[0] = w > 0 ? (float) Math.sqrt(w) : 0;
+ }
+ Q[1] = rv[0];
+ Q[2] = rv[1];
+ Q[3] = rv[2];
+ }
+
+ /// Re-expresses `inR` for a device held so that its `X` axis lies along
+ /// world axis `X` and its `Y` along `Y` (the AXIS_* constants), the way
+ /// landscape compass and camera code calls it.
+ static boolean remapCoordinateSystem(float[] inR, int X, int Y, float[] outR) {
+ if (inR == outR) {
+ float[] temp = new float[outR.length];
+ boolean ok = remap(inR, X, Y, temp);
+ if (ok) {
+ System.arraycopy(temp, 0, outR, 0, outR.length);
+ }
+ return ok;
+ }
+ return remap(inR, X, Y, outR);
+ }
+
+ private static boolean remap(float[] inR, int X, int Y, float[] outR) {
+ if ((X & 0x7C) != 0 || (Y & 0x7C) != 0) {
+ return false;
+ }
+ if ((X & 0x3) == 0 || (Y & 0x3) == 0) {
+ return false;
+ }
+ if ((X & 0x3) == (Y & 0x3)) {
+ return false;
+ }
+ // Z is X cross Y; its sign follows from the axes and their signs.
+ int Z = X ^ Y;
+ final int x = (X & 0x3) - 1;
+ final int y = (Y & 0x3) - 1;
+ final int z = (Z & 0x3) - 1;
+ final int axisY = (z + 1) % 3;
+ final int axisZ = (z + 2) % 3;
+ if (((x ^ axisY) | (y ^ axisZ)) != 0) {
+ Z ^= 0x80;
+ }
+ final boolean sx = (X >= 0x80);
+ final boolean sy = (Y >= 0x80);
+ final boolean sz = (Z >= 0x80);
+ final int rowLength = (outR.length == 16) ? 4 : 3;
+ for (int j = 0; j < 3; j++) {
+ final int offset = j * rowLength;
+ for (int i = 0; i < 3; i++) {
+ if (x == i) {
+ outR[offset + i] = sx ? -inR[offset + 0] : inR[offset + 0];
+ }
+ if (y == i) {
+ outR[offset + i] = sy ? -inR[offset + 1] : inR[offset + 1];
+ }
+ if (z == i) {
+ outR[offset + i] = sz ? -inR[offset + 2] : inR[offset + 2];
+ }
+ }
+ }
+ if (outR.length == 16) {
+ outR[3] = outR[7] = outR[11] = outR[12] = outR[13] = outR[14] = 0;
+ outR[15] = 1;
+ }
+ return true;
+ }
+
+ /// The angle change between two rotation matrices, as Android defines it.
+ static void getAngleChange(float[] angleChange, float[] R, float[] prevR) {
+ float rd1, rd4, rd6, rd7, rd8;
+ float ri0, ri1, ri2, ri3, ri4, ri5, ri6, ri7, ri8;
+ float pri0, pri1, pri2, pri3, pri4, pri5, pri6, pri7, pri8;
+ if (R.length == 9) {
+ ri0 = R[0]; ri1 = R[1]; ri2 = R[2]; ri3 = R[3]; ri4 = R[4];
+ ri5 = R[5]; ri6 = R[6]; ri7 = R[7]; ri8 = R[8];
+ } else {
+ ri0 = R[0]; ri1 = R[1]; ri2 = R[2]; ri3 = R[4]; ri4 = R[5];
+ ri5 = R[6]; ri6 = R[8]; ri7 = R[9]; ri8 = R[10];
+ }
+ if (prevR.length == 9) {
+ pri0 = prevR[0]; pri1 = prevR[1]; pri2 = prevR[2]; pri3 = prevR[3]; pri4 = prevR[4];
+ pri5 = prevR[5]; pri6 = prevR[6]; pri7 = prevR[7]; pri8 = prevR[8];
+ } else {
+ pri0 = prevR[0]; pri1 = prevR[1]; pri2 = prevR[2]; pri3 = prevR[4]; pri4 = prevR[5];
+ pri5 = prevR[6]; pri6 = prevR[8]; pri7 = prevR[9]; pri8 = prevR[10];
+ }
+ rd1 = pri0 * ri1 + pri3 * ri4 + pri6 * ri7;
+ rd4 = pri1 * ri1 + pri4 * ri4 + pri7 * ri7;
+ rd6 = pri2 * ri0 + pri5 * ri3 + pri8 * ri6;
+ rd7 = pri2 * ri1 + pri5 * ri4 + pri8 * ri7;
+ rd8 = pri2 * ri2 + pri5 * ri5 + pri8 * ri8;
+ angleChange[0] = (float) MathUtil.atan2(rd1, rd4);
+ angleChange[1] = (float) MathUtil.asin(-rd7);
+ angleChange[2] = (float) MathUtil.atan2(-rd6, rd8);
+ }
+
+ /// Altitude in meters from the pressure at sea level and here, in hPa.
+ static float getAltitude(float p0, float p) {
+ final float coef = 1.0f / 5.255f;
+ return 44330.0f * (1.0f - (float) MathUtil.pow(p / p0, coef));
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/location/Criteria.java b/maven/android-compat/src/main/java/android/location/Criteria.java
new file mode 100644
index 00000000000..3ac78ca609e
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/location/Criteria.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.location;
+
+/// What a caller wants from a provider; only the accuracy is consulted by
+/// [LocationManager#getBestProvider(Criteria, boolean)].
+public class Criteria {
+
+ public static final int NO_REQUIREMENT = 0;
+ public static final int POWER_LOW = 1;
+ public static final int POWER_MEDIUM = 2;
+ public static final int POWER_HIGH = 3;
+ public static final int ACCURACY_FINE = 1;
+ public static final int ACCURACY_COARSE = 2;
+ public static final int ACCURACY_LOW = 1;
+ public static final int ACCURACY_MEDIUM = 2;
+ public static final int ACCURACY_HIGH = 3;
+
+ private int accuracy = NO_REQUIREMENT;
+ private int powerRequirement = NO_REQUIREMENT;
+ private int horizontalAccuracy = NO_REQUIREMENT;
+ private int verticalAccuracy = NO_REQUIREMENT;
+ private int speedAccuracy = NO_REQUIREMENT;
+ private int bearingAccuracy = NO_REQUIREMENT;
+ private boolean altitudeRequired;
+ private boolean bearingRequired;
+ private boolean speedRequired;
+ private boolean costAllowed = true;
+
+ public Criteria() {
+ }
+
+ public Criteria(Criteria c) {
+ accuracy = c.accuracy;
+ powerRequirement = c.powerRequirement;
+ horizontalAccuracy = c.horizontalAccuracy;
+ verticalAccuracy = c.verticalAccuracy;
+ speedAccuracy = c.speedAccuracy;
+ bearingAccuracy = c.bearingAccuracy;
+ altitudeRequired = c.altitudeRequired;
+ bearingRequired = c.bearingRequired;
+ speedRequired = c.speedRequired;
+ costAllowed = c.costAllowed;
+ }
+
+ public int getAccuracy() {
+ return accuracy;
+ }
+
+ public void setAccuracy(int accuracy) {
+ this.accuracy = accuracy;
+ }
+
+ public int getPowerRequirement() {
+ return powerRequirement;
+ }
+
+ public void setPowerRequirement(int level) {
+ powerRequirement = level;
+ }
+
+ public int getHorizontalAccuracy() {
+ return horizontalAccuracy;
+ }
+
+ public void setHorizontalAccuracy(int accuracy) {
+ horizontalAccuracy = accuracy;
+ }
+
+ public int getVerticalAccuracy() {
+ return verticalAccuracy;
+ }
+
+ public void setVerticalAccuracy(int accuracy) {
+ verticalAccuracy = accuracy;
+ }
+
+ public int getSpeedAccuracy() {
+ return speedAccuracy;
+ }
+
+ public void setSpeedAccuracy(int accuracy) {
+ speedAccuracy = accuracy;
+ }
+
+ public int getBearingAccuracy() {
+ return bearingAccuracy;
+ }
+
+ public void setBearingAccuracy(int accuracy) {
+ bearingAccuracy = accuracy;
+ }
+
+ public boolean isAltitudeRequired() {
+ return altitudeRequired;
+ }
+
+ public void setAltitudeRequired(boolean required) {
+ altitudeRequired = required;
+ }
+
+ public boolean isBearingRequired() {
+ return bearingRequired;
+ }
+
+ public void setBearingRequired(boolean required) {
+ bearingRequired = required;
+ }
+
+ public boolean isSpeedRequired() {
+ return speedRequired;
+ }
+
+ public void setSpeedRequired(boolean required) {
+ speedRequired = required;
+ }
+
+ public boolean isCostAllowed() {
+ return costAllowed;
+ }
+
+ public void setCostAllowed(boolean allowed) {
+ costAllowed = allowed;
+ }
+
+ /// True when the caller asked for anything finer than coarse.
+ boolean wantsFine() {
+ return accuracy == ACCURACY_FINE || horizontalAccuracy == ACCURACY_HIGH
+ || powerRequirement == POWER_HIGH;
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/location/Location.java b/maven/android-compat/src/main/java/android/location/Location.java
new file mode 100644
index 00000000000..cdc7f597422
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/location/Location.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.location;
+
+/// A geographic fix, as Android describes one. Fixes reported by
+/// [LocationManager] are converted from `com.codename1.location.Location`;
+/// the `has*` flags say which of the optional values the platform supplied.
+public class Location {
+
+ private static final double EARTH_RADIUS_METERS = 6371008.8;
+
+ private String provider;
+ private double latitude;
+ private double longitude;
+ private double altitude;
+ private float accuracy;
+ private float bearing;
+ private float speed;
+ private long time;
+ private long elapsedRealtimeNanos;
+ private boolean hasAltitude;
+ private boolean hasAccuracy;
+ private boolean hasBearing;
+ private boolean hasSpeed;
+ private android.os.Bundle extras;
+
+ public Location(String provider) {
+ this.provider = provider;
+ }
+
+ public Location(Location l) {
+ set(l);
+ }
+
+ public void set(Location l) {
+ provider = l.provider;
+ latitude = l.latitude;
+ longitude = l.longitude;
+ altitude = l.altitude;
+ accuracy = l.accuracy;
+ bearing = l.bearing;
+ speed = l.speed;
+ time = l.time;
+ elapsedRealtimeNanos = l.elapsedRealtimeNanos;
+ hasAltitude = l.hasAltitude;
+ hasAccuracy = l.hasAccuracy;
+ hasBearing = l.hasBearing;
+ hasSpeed = l.hasSpeed;
+ extras = l.extras == null ? null : new android.os.Bundle(l.extras);
+ }
+
+ public void reset() {
+ set(new Location((String) null));
+ }
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public void setProvider(String provider) {
+ this.provider = provider;
+ }
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = latitude;
+ }
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = longitude;
+ }
+
+ public boolean hasAltitude() {
+ return hasAltitude;
+ }
+
+ public double getAltitude() {
+ return altitude;
+ }
+
+ public void setAltitude(double altitude) {
+ this.altitude = altitude;
+ hasAltitude = true;
+ }
+
+ public void removeAltitude() {
+ altitude = 0;
+ hasAltitude = false;
+ }
+
+ public boolean hasAccuracy() {
+ return hasAccuracy;
+ }
+
+ public float getAccuracy() {
+ return accuracy;
+ }
+
+ public void setAccuracy(float accuracy) {
+ this.accuracy = accuracy;
+ hasAccuracy = true;
+ }
+
+ public void removeAccuracy() {
+ accuracy = 0;
+ hasAccuracy = false;
+ }
+
+ public boolean hasBearing() {
+ return hasBearing;
+ }
+
+ public float getBearing() {
+ return bearing;
+ }
+
+ /// Normalized into [0, 360), as Android does.
+ public void setBearing(float bearing) {
+ float b = bearing % 360f;
+ if (b < 0) {
+ b += 360f;
+ }
+ this.bearing = b;
+ hasBearing = true;
+ }
+
+ public void removeBearing() {
+ bearing = 0;
+ hasBearing = false;
+ }
+
+ public boolean hasSpeed() {
+ return hasSpeed;
+ }
+
+ public float getSpeed() {
+ return speed;
+ }
+
+ public void setSpeed(float speed) {
+ this.speed = speed;
+ hasSpeed = true;
+ }
+
+ public void removeSpeed() {
+ speed = 0;
+ hasSpeed = false;
+ }
+
+ public long getTime() {
+ return time;
+ }
+
+ public void setTime(long time) {
+ this.time = time;
+ }
+
+ public long getElapsedRealtimeNanos() {
+ return elapsedRealtimeNanos;
+ }
+
+ public void setElapsedRealtimeNanos(long nanos) {
+ elapsedRealtimeNanos = nanos;
+ }
+
+ public long getElapsedRealtimeMillis() {
+ return elapsedRealtimeNanos / 1000000L;
+ }
+
+ public android.os.Bundle getExtras() {
+ return extras;
+ }
+
+ public void setExtras(android.os.Bundle extras) {
+ this.extras = extras;
+ }
+
+ /// No mock providers exist here.
+ public boolean isMock() {
+ return false;
+ }
+
+ public boolean isFromMockProvider() {
+ return false;
+ }
+
+ public float distanceTo(Location dest) {
+ float[] r = new float[2];
+ computeDistanceAndBearing(latitude, longitude, dest.latitude, dest.longitude, r);
+ return r[0];
+ }
+
+ public float bearingTo(Location dest) {
+ float[] r = new float[2];
+ computeDistanceAndBearing(latitude, longitude, dest.latitude, dest.longitude, r);
+ return r[1];
+ }
+
+ /// `results[0]` receives the distance in meters, `results[1]` (when the
+ /// array is long enough) the initial bearing and `results[2]` the final
+ /// bearing, in degrees east of true north.
+ ///
+ /// Android solves Vincenty's inverse problem on the WGS84 ellipsoid; this
+ /// uses the haversine formula on a sphere of the mean earth radius, which
+ /// differs from it by up to about 0.5%. That is well inside GPS error for
+ /// the distances a phone measures, but a survey-grade comparison against a
+ /// value Android computed will not match to the meter.
+ public static void distanceBetween(double startLatitude, double startLongitude,
+ double endLatitude, double endLongitude, float[] results) {
+ if (results == null || results.length < 1) {
+ throw new IllegalArgumentException("results is null or has length < 1");
+ }
+ computeDistanceAndBearing(startLatitude, startLongitude, endLatitude, endLongitude, results);
+ }
+
+ private static void computeDistanceAndBearing(double lat1, double lon1, double lat2, double lon2,
+ float[] results) {
+ double p1 = Math.toRadians(lat1);
+ double p2 = Math.toRadians(lat2);
+ double dp = p2 - p1;
+ double dl = Math.toRadians(lon2 - lon1);
+ double sinDp = Math.sin(dp / 2);
+ double sinDl = Math.sin(dl / 2);
+ double a = sinDp * sinDp + Math.cos(p1) * Math.cos(p2) * sinDl * sinDl;
+ double c = 2 * com.codename1.util.MathUtil.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ results[0] = (float) (EARTH_RADIUS_METERS * c);
+ if (results.length > 1) {
+ results[1] = initialBearing(p1, p2, dl);
+ }
+ if (results.length > 2) {
+ // The final bearing is the reverse course's initial bearing, turned around.
+ float back = initialBearing(p2, p1, -dl);
+ results[2] = (float) ((back + 180.0) % 360.0);
+ }
+ }
+
+ private static float initialBearing(double p1, double p2, double dl) {
+ double y = Math.sin(dl) * Math.cos(p2);
+ double x = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(dl);
+ double deg = Math.toDegrees(com.codename1.util.MathUtil.atan2(y, x));
+ // Android reports bearings in (-180, 180].
+ if (deg <= -180.0) {
+ deg += 360.0;
+ }
+ return (float) deg;
+ }
+
+ @Override
+ public String toString() {
+ return "Location[" + provider + " " + latitude + "," + longitude
+ + (hasAccuracy ? " hAcc=" + accuracy : "") + " et=" + getElapsedRealtimeMillis()
+ + (hasAltitude ? " alt=" + altitude : "")
+ + (hasSpeed ? " vel=" + speed : "")
+ + (hasBearing ? " bear=" + bearing : "") + "]";
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/location/LocationListener.java b/maven/android-compat/src/main/java/android/location/LocationListener.java
new file mode 100644
index 00000000000..2582792f103
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/location/LocationListener.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.location;
+
+/// Receives fixes from [LocationManager#requestLocationUpdates].
+public interface LocationListener {
+
+ void onLocationChanged(Location location);
+
+ default void onLocationChanged(java.util.List locations) {
+ for (int i = 0; i < locations.size(); i++) {
+ onLocationChanged(locations.get(i));
+ }
+ }
+
+ default void onFlushComplete(int requestCode) {
+ }
+
+ default void onProviderEnabled(String provider) {
+ }
+
+ default void onProviderDisabled(String provider) {
+ }
+
+ /// Deprecated on Android and never called there since API 29; never
+ /// called here either.
+ default void onStatusChanged(String provider, int status, android.os.Bundle extras) {
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/location/LocationManager.java b/maven/android-compat/src/main/java/android/location/LocationManager.java
new file mode 100644
index 00000000000..f8bb10f485e
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/location/LocationManager.java
@@ -0,0 +1,548 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.location;
+
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import com.codename1.androidcompat.runtime.CompatReport;
+import com.codename1.androidcompat.runtime.MainThread;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/// Android's location service over `com.codename1.location.LocationManager`.
+///
+/// Codename One has one location stream per application and one listener on
+/// it, where Android has any number of registrations, each with its own
+/// provider, interval and distance filter. This class owns the single
+/// Codename One listener while at least one registration exists and fans each
+/// fix out to every registration whose `minTimeMs` and `minDistanceM` it
+/// passes. The stream is requested at the shortest interval and the finest
+/// accuracy any registration asked for.
+///
+/// Providers are names only: there is one platform stream, and `gps`,
+/// `network` and `fused` registrations all receive it (with the accuracy the
+/// platform reports). `passive` registrations receive it too, which is what
+/// Android's passive provider does when another app is not involved.
+///
+/// The `PendingIntent` overloads are not provided. Background delivery to a
+/// killed application is a platform feature; on iOS keeping the stream alive
+/// in the background needs the `ios.background_modes=location` build hint,
+/// and a foreground [android.app.Service] keeps the application running on
+/// Android.
+///
+/// Registrations are kept on the main thread, which is Codename One's EDT; a
+/// call from another thread is posted there rather than synchronized.
+public class LocationManager {
+
+ public static final String GPS_PROVIDER = "gps";
+ public static final String NETWORK_PROVIDER = "network";
+ public static final String PASSIVE_PROVIDER = "passive";
+ public static final String FUSED_PROVIDER = "fused";
+ public static final String KEY_LOCATION_CHANGED = "location";
+ public static final String KEY_PROVIDER_ENABLED = "providerEnabled";
+ public static final String KEY_STATUS_CHANGED = "status";
+ public static final String PROVIDERS_CHANGED_ACTION = "android.location.PROVIDERS_CHANGED";
+ public static final String MODE_CHANGED_ACTION = "android.location.MODE_CHANGED";
+
+ /// How long [#getCurrentLocation] waits for a fix before answering null,
+ /// as Android's does after roughly thirty seconds.
+ static final long CURRENT_LOCATION_TIMEOUT_MS = 30000L;
+
+ /// The Codename One manager to use instead of the platform's; set by tests.
+ static com.codename1.location.LocationManager override;
+
+ private static LocationManager instance;
+
+ private final List registrations = new ArrayList();
+ private boolean listening;
+ private long requestedInterval = -1;
+ private int requestedPriority = -1;
+
+ private final com.codename1.location.LocationListener bridge = new com.codename1.location.LocationListener() {
+ @Override
+ public void locationUpdated(com.codename1.location.Location location) {
+ deliver(location);
+ }
+
+ @Override
+ public void providerStateChanged(int newState) {
+ providerState(newState);
+ }
+ };
+
+ /// Context.getSystemService(LOCATION_SERVICE) answers one manager per
+ /// application, as Android does.
+ public static LocationManager getInstance() {
+ if (instance == null) {
+ instance = new LocationManager();
+ }
+ return instance;
+ }
+
+ LocationManager() {
+ }
+
+ static com.codename1.location.LocationManager platform() {
+ if (override != null) {
+ return override;
+ }
+ return com.codename1.location.LocationManager.getLocationManager();
+ }
+
+ // ------------------------------------------------------------- providers
+
+ public List getAllProviders() {
+ List out = new ArrayList();
+ out.add(PASSIVE_PROVIDER);
+ out.add(GPS_PROVIDER);
+ out.add(NETWORK_PROVIDER);
+ out.add(FUSED_PROVIDER);
+ return out;
+ }
+
+ public List getProviders(boolean enabledOnly) {
+ List out = new ArrayList();
+ for (String p : getAllProviders()) {
+ if (!enabledOnly || isProviderEnabled(p)) {
+ out.add(p);
+ }
+ }
+ return out;
+ }
+
+ public List getProviders(Criteria criteria, boolean enabledOnly) {
+ return getProviders(enabledOnly);
+ }
+
+ public String getBestProvider(Criteria criteria, boolean enabledOnly) {
+ String best = criteria == null || criteria.wantsFine() ? GPS_PROVIDER : NETWORK_PROVIDER;
+ if (enabledOnly && !isProviderEnabled(best)) {
+ return isProviderEnabled(NETWORK_PROVIDER) ? NETWORK_PROVIDER : null;
+ }
+ return best;
+ }
+
+ public boolean hasProvider(String provider) {
+ return getAllProviders().contains(provider);
+ }
+
+ public boolean isProviderEnabled(String provider) {
+ if (!hasProvider(provider)) {
+ return false;
+ }
+ com.codename1.location.LocationManager lm = platform();
+ if (lm == null) {
+ return false;
+ }
+ if (GPS_PROVIDER.equals(provider) && lm.isGPSDetectionSupported()) {
+ return lm.isGPSEnabled();
+ }
+ return lm.getStatus() != com.codename1.location.LocationManager.OUT_OF_SERVICE;
+ }
+
+ public boolean isLocationEnabled() {
+ return isProviderEnabled(GPS_PROVIDER) || isProviderEnabled(NETWORK_PROVIDER);
+ }
+
+ // ------------------------------------------------------------ last fix
+
+ public Location getLastKnownLocation(String provider) {
+ if (!hasProvider(provider)) {
+ throw new IllegalArgumentException("provider doesn't exist: " + provider);
+ }
+ com.codename1.location.LocationManager lm = platform();
+ if (lm == null) {
+ return null;
+ }
+ com.codename1.location.Location l = lm.getLastKnownLocation();
+ return l == null ? null : convert(l, provider);
+ }
+
+ /// Answers the next fix, or null when none arrives within thirty
+ /// seconds or `cancellationSignal` is cancelled first (then nothing is
+ /// answered at all, as on Android).
+ public void getCurrentLocation(String provider, CancellationSignal cancellationSignal,
+ Executor executor, final Consumer consumer) {
+ if (consumer == null) {
+ throw new IllegalArgumentException("consumer is null");
+ }
+ final Registration r = new Registration(provider, 0, 0, null, executor, null);
+ r.consumer = consumer;
+ r.oneShot = true;
+ if (cancellationSignal != null) {
+ cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
+ @Override
+ public void onCancel() {
+ remove(r);
+ }
+ });
+ }
+ add(r);
+ new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (registrations.contains(r)) {
+ remove(r);
+ r.dispatch(null);
+ }
+ }
+ }, CURRENT_LOCATION_TIMEOUT_MS);
+ }
+
+ public void requestSingleUpdate(String provider, LocationListener listener, Looper looper) {
+ Registration r = new Registration(provider, 0, 0, listener, null, looper);
+ r.oneShot = true;
+ add(r);
+ }
+
+ public void requestSingleUpdate(Criteria criteria, LocationListener listener, Looper looper) {
+ requestSingleUpdate(getBestProvider(criteria, false), listener, looper);
+ }
+
+ // -------------------------------------------------------------- updates
+
+ public void requestLocationUpdates(String provider, long minTimeMs, float minDistanceM,
+ LocationListener listener) {
+ requestLocationUpdates(provider, minTimeMs, minDistanceM, listener, (Looper) null);
+ }
+
+ public void requestLocationUpdates(String provider, long minTimeMs, float minDistanceM,
+ LocationListener listener, Looper looper) {
+ if (listener == null) {
+ throw new IllegalArgumentException("invalid null listener");
+ }
+ if (!hasProvider(provider)) {
+ throw new IllegalArgumentException("provider doesn't exist: " + provider);
+ }
+ replace(new Registration(provider, minTimeMs, minDistanceM, listener, null, looper));
+ }
+
+ public void requestLocationUpdates(String provider, long minTimeMs, float minDistanceM,
+ Executor executor, LocationListener listener) {
+ if (listener == null || executor == null) {
+ throw new IllegalArgumentException("invalid null listener or executor");
+ }
+ if (!hasProvider(provider)) {
+ throw new IllegalArgumentException("provider doesn't exist: " + provider);
+ }
+ replace(new Registration(provider, minTimeMs, minDistanceM, listener, executor, null));
+ }
+
+ public void requestLocationUpdates(long minTimeMs, float minDistanceM, Criteria criteria,
+ LocationListener listener, Looper looper) {
+ requestLocationUpdates(getBestProvider(criteria, false), minTimeMs, minDistanceM, listener, looper);
+ }
+
+ public void removeUpdates(final LocationListener listener) {
+ onMain(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = registrations.size() - 1; i >= 0; i--) {
+ if (registrations.get(i).listener == listener) {
+ registrations.remove(i).removed.set(true);
+ }
+ }
+ reconfigure();
+ }
+ });
+ }
+
+ /// Codename One keeps no per-provider queue to flush; completion is
+ /// reported straight away.
+ public void requestFlush(String provider, final LocationListener listener, final int requestCode) {
+ onMain(new ProviderCall(listener, null, requestCode));
+ }
+
+ /// One listener callback, posted where the registration wants it.
+ private static final class ProviderCall implements Runnable {
+ private final LocationListener listener;
+ private final String provider;
+ private final int code;
+
+ /// `provider` null means a flush with request `code`; otherwise
+ /// `code` is 1 for enabled and 0 for disabled.
+ ProviderCall(LocationListener listener, String provider, int code) {
+ this.listener = listener;
+ this.provider = provider;
+ this.code = code;
+ }
+
+ @Override
+ public void run() {
+ if (provider == null) {
+ listener.onFlushComplete(code);
+ } else if (code == 1) {
+ listener.onProviderEnabled(provider);
+ } else {
+ listener.onProviderDisabled(provider);
+ }
+ }
+ }
+
+ /// Geofences through a `PendingIntent` have no counterpart that does not
+ /// need reflection; use `com.codename1.location.GeofenceManager`.
+ public void addProximityAlert(double latitude, double longitude, float radius, long expiration,
+ Object intent) {
+ CompatReport.unsupported("LocationManager", "addProximityAlert");
+ }
+
+ // -------------------------------------------------------- registrations
+
+ private void replace(final Registration r) {
+ onMain(new Runnable() {
+ @Override
+ public void run() {
+ // Android replaces an existing registration of the same listener.
+ for (int i = registrations.size() - 1; i >= 0; i--) {
+ if (registrations.get(i).listener == r.listener) {
+ registrations.remove(i).removed.set(true);
+ }
+ }
+ registrations.add(r);
+ reconfigure();
+ }
+ });
+ }
+
+ private void add(final Registration r) {
+ onMain(new Runnable() {
+ @Override
+ public void run() {
+ registrations.add(r);
+ reconfigure();
+ }
+ });
+ }
+
+ private void remove(final Registration r) {
+ onMain(new Runnable() {
+ @Override
+ public void run() {
+ registrations.remove(r);
+ r.removed.set(true);
+ reconfigure();
+ }
+ });
+ }
+
+ private static void onMain(Runnable r) {
+ if (MainThread.isMainThread()) {
+ r.run();
+ } else {
+ MainThread.post(r);
+ }
+ }
+
+ /// Installs, retunes or removes the one Codename One listener so it
+ /// matches what the registrations ask for.
+ private void reconfigure() {
+ com.codename1.location.LocationManager lm = platform();
+ if (lm == null) {
+ return;
+ }
+ if (registrations.isEmpty()) {
+ if (listening) {
+ lm.setLocationListener(null);
+ listening = false;
+ requestedInterval = -1;
+ requestedPriority = -1;
+ }
+ return;
+ }
+ long interval = Long.MAX_VALUE;
+ int priority = com.codename1.location.LocationRequest.PRIORITY_LOW_ACCUARCY;
+ for (int i = 0; i < registrations.size(); i++) {
+ Registration r = registrations.get(i);
+ interval = Math.min(interval, r.minTime);
+ priority = Math.min(priority, r.priority());
+ }
+ if (listening && interval == requestedInterval && priority == requestedPriority) {
+ return;
+ }
+ // Codename One's request takes an int interval; a minTime of zero
+ // asks for fixes as fast as the platform gives them.
+ int cn1Interval = (int) Math.min(Integer.MAX_VALUE, Math.max(0, interval));
+ lm.setLocationListener(bridge, new com.codename1.location.LocationRequest(priority, cn1Interval));
+ listening = true;
+ requestedInterval = interval;
+ requestedPriority = priority;
+ }
+
+ void deliver(com.codename1.location.Location fix) {
+ if (fix == null) {
+ return;
+ }
+ // Copied first: a one-shot registration removes itself while the
+ // list is being walked, and a listener may register or remove others.
+ Registration[] snapshot = registrations.toArray(new Registration[registrations.size()]);
+ boolean removed = false;
+ for (int i = 0; i < snapshot.length; i++) {
+ Registration r = snapshot[i];
+ if (!registrations.contains(r)) {
+ // Removed by an earlier listener's callback in this pass.
+ continue;
+ }
+ Location l = convert(fix, r.provider);
+ if (!r.accepts(l)) {
+ continue;
+ }
+ if (r.oneShot) {
+ // Its one fix is the one dispatched below, so it is not
+ // flagged removed.
+ registrations.remove(r);
+ removed = true;
+ }
+ r.dispatch(l);
+ }
+ if (removed) {
+ reconfigure();
+ }
+ }
+
+ void providerState(int state) {
+ Registration[] snapshot = registrations.toArray(new Registration[registrations.size()]);
+ for (int i = 0; i < snapshot.length; i++) {
+ final Registration r = snapshot[i];
+ if (!registrations.contains(r)) {
+ continue;
+ }
+ if (r.listener == null) {
+ continue;
+ }
+ if (state == com.codename1.location.LocationManager.OUT_OF_SERVICE) {
+ r.run(new ProviderCall(r.listener, r.provider, 0));
+ } else if (state == com.codename1.location.LocationManager.AVAILABLE) {
+ r.run(new ProviderCall(r.listener, r.provider, 1));
+ }
+ }
+ }
+
+ /// The Codename One fix as an Android one. Codename One reports zero for
+ /// a value the platform did not supply, so a zero accuracy, speed or
+ /// direction is left unset rather than reported as a measurement.
+ static Location convert(com.codename1.location.Location src, String provider) {
+ Location l = new Location(provider);
+ l.setLatitude(src.getLatitude());
+ l.setLongitude(src.getLongitude());
+ if (src.getAltitude() != 0) {
+ l.setAltitude(src.getAltitude());
+ }
+ if (src.getAccuracy() > 0) {
+ l.setAccuracy(src.getAccuracy());
+ }
+ if (src.getVelocity() > 0) {
+ l.setSpeed(src.getVelocity());
+ }
+ if (src.getDirection() > 0) {
+ l.setBearing(src.getDirection());
+ }
+ long ts = src.getTimeStamp();
+ l.setTime(ts > 0 ? ts : System.currentTimeMillis());
+ l.setElapsedRealtimeNanos(android.os.SystemClock.elapsedRealtimeNanos());
+ return l;
+ }
+
+ /// One `requestLocationUpdates` (or one-shot request).
+ static final class Registration {
+ /// Set when the registration is removed; a fix already posted to
+ /// its looper or executor is then dropped, so a removed listener
+ /// hears nothing more.
+ final java.util.concurrent.atomic.AtomicBoolean removed = new java.util.concurrent.atomic.AtomicBoolean();
+ final String provider;
+ final long minTime;
+ final float minDistance;
+ final LocationListener listener;
+ final Executor executor;
+ final Looper looper;
+ Consumer consumer;
+ boolean oneShot;
+ private Location last;
+
+ Registration(String provider, long minTime, float minDistance, LocationListener listener,
+ Executor executor, Looper looper) {
+ this.provider = provider;
+ this.minTime = Math.max(0, minTime);
+ this.minDistance = Math.max(0, minDistance);
+ this.listener = listener;
+ this.executor = executor;
+ this.looper = looper;
+ }
+
+ int priority() {
+ if (NETWORK_PROVIDER.equals(provider)) {
+ return com.codename1.location.LocationRequest.PRIORITY_MEDIUM_ACCUARCY;
+ }
+ if (PASSIVE_PROVIDER.equals(provider)) {
+ return com.codename1.location.LocationRequest.PRIORITY_LOW_ACCUARCY;
+ }
+ return com.codename1.location.LocationRequest.PRIORITY_HIGH_ACCUARCY;
+ }
+
+ /// Android's filter: at least `minTime` since the last fix this
+ /// registration was given, and at least `minDistance` from it.
+ boolean accepts(Location l) {
+ if (last == null) {
+ last = l;
+ return true;
+ }
+ if (l.getTime() - last.getTime() < minTime) {
+ return false;
+ }
+ if (minDistance > 0 && last.distanceTo(l) < minDistance) {
+ return false;
+ }
+ last = l;
+ return true;
+ }
+
+ void dispatch(final Location l) {
+ run(new Runnable() {
+ @Override
+ public void run() {
+ if (consumer == null && removed.get()) {
+ return;
+ }
+ if (consumer != null) {
+ consumer.accept(l);
+ } else if (l != null) {
+ listener.onLocationChanged(l);
+ }
+ }
+ });
+ }
+
+ void run(Runnable r) {
+ if (executor != null) {
+ executor.execute(r);
+ } else if (looper != null && !looper.isCurrentThread()) {
+ new Handler(looper).post(r);
+ } else {
+ r.run();
+ }
+ }
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/os/Binder.java b/maven/android-compat/src/main/java/android/os/Binder.java
new file mode 100644
index 00000000000..ef4d77bc176
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/os/Binder.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.os;
+
+/// A local binder. Subclass it and return the service from a method of your
+/// own, as Android's bound-service guide does.
+public class Binder implements IBinder {
+
+ private String descriptor;
+
+ public Binder() {
+ }
+
+ public Binder(String descriptor) {
+ this.descriptor = descriptor;
+ }
+
+ public void attachInterface(Object owner, String descriptor) {
+ this.descriptor = descriptor;
+ }
+
+ @Override
+ public String getInterfaceDescriptor() {
+ return descriptor;
+ }
+
+ @Override
+ public boolean pingBinder() {
+ return true;
+ }
+
+ @Override
+ public boolean isBinderAlive() {
+ return true;
+ }
+
+ @Override
+ public void linkToDeath(DeathRecipient recipient, int flags) {
+ }
+
+ @Override
+ public boolean unlinkToDeath(DeathRecipient recipient, int flags) {
+ return true;
+ }
+
+ /// There is one process and no other caller; Android answers the
+ /// caller's own ids for a local call, and these stand in for them.
+ public static int getCallingPid() {
+ return 1;
+ }
+
+ public static int getCallingUid() {
+ return 10000;
+ }
+
+ public static long clearCallingIdentity() {
+ return 0;
+ }
+
+ public static void restoreCallingIdentity(long token) {
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/os/IBinder.java b/maven/android-compat/src/main/java/android/os/IBinder.java
new file mode 100644
index 00000000000..2389bc4deb8
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/os/IBinder.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.os;
+
+/// What `Service.onBind` answers. Within one process, as here, a client
+/// casts it back to the service's own `Binder` subclass, which is the local
+/// binder pattern Android documents; there is no cross-process transaction.
+public interface IBinder {
+
+ int FIRST_CALL_TRANSACTION = 0x00000001;
+ int LAST_CALL_TRANSACTION = 0x00ffffff;
+ int FLAG_ONEWAY = 0x00000001;
+
+ /// Notified when the binder's process dies, which never happens apart
+ /// from the application's own.
+ interface DeathRecipient {
+ void binderDied();
+ }
+
+ String getInterfaceDescriptor();
+
+ boolean pingBinder();
+
+ boolean isBinderAlive();
+
+ void linkToDeath(DeathRecipient recipient, int flags);
+
+ boolean unlinkToDeath(DeathRecipient recipient, int flags);
+}
diff --git a/maven/android-compat/src/main/java/android/security/keystore/KeyGenParameterSpec.java b/maven/android-compat/src/main/java/android/security/keystore/KeyGenParameterSpec.java
new file mode 100644
index 00000000000..e9f5639f4a9
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/security/keystore/KeyGenParameterSpec.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.security.keystore;
+
+/// Android's `KeyGenParameterSpec`: what `KeyGenerator.init` is told about a
+/// key that lives in the `AndroidKeyStore`.
+///
+/// It implements the JDK's `AlgorithmParameterSpec` by that name, because the
+/// application passes it to `javax.crypto.KeyGenerator`; the remap step points
+/// both at the runtime's shim. The user-authentication settings are recorded
+/// and answered back but never enforced -- see
+/// `com.codename1.androidcompat.jdk.KeyStore` for where keys are kept.
+public final class KeyGenParameterSpec implements java.security.spec.AlgorithmParameterSpec {
+ private final String keystoreAlias;
+ private final int purposes;
+ private final int keySize;
+ private final String[] blockModes;
+ private final String[] encryptionPaddings;
+ private final String[] digests;
+ private final boolean randomizedEncryptionRequired;
+ private final boolean userAuthenticationRequired;
+ private final int userAuthenticationValidityDurationSeconds;
+ private final boolean strongBoxBacked;
+ private final boolean invalidatedByBiometricEnrollment;
+
+ KeyGenParameterSpec(Builder b) {
+ keystoreAlias = b.keystoreAlias;
+ purposes = b.purposes;
+ keySize = b.keySize;
+ blockModes = copy(b.blockModes);
+ encryptionPaddings = copy(b.encryptionPaddings);
+ digests = copy(b.digests);
+ randomizedEncryptionRequired = b.randomizedEncryptionRequired;
+ userAuthenticationRequired = b.userAuthenticationRequired;
+ userAuthenticationValidityDurationSeconds = b.userAuthenticationValidityDurationSeconds;
+ strongBoxBacked = b.strongBoxBacked;
+ invalidatedByBiometricEnrollment = b.invalidatedByBiometricEnrollment;
+ }
+
+ static String[] copy(String[] a) {
+ if (a == null) {
+ return new String[0];
+ }
+ String[] out = new String[a.length];
+ System.arraycopy(a, 0, out, 0, a.length);
+ return out;
+ }
+
+ public String getKeystoreAlias() {
+ return keystoreAlias;
+ }
+
+ public int getPurposes() {
+ return purposes;
+ }
+
+ /// The requested size in bits, or -1 when the spec does not say.
+ public int getKeySize() {
+ return keySize;
+ }
+
+ public String[] getBlockModes() {
+ return copy(blockModes);
+ }
+
+ public String[] getEncryptionPaddings() {
+ return copy(encryptionPaddings);
+ }
+
+ public String[] getDigests() {
+ return copy(digests);
+ }
+
+ public boolean isDigestsSpecified() {
+ return digests.length > 0;
+ }
+
+ public boolean isRandomizedEncryptionRequired() {
+ return randomizedEncryptionRequired;
+ }
+
+ public boolean isUserAuthenticationRequired() {
+ return userAuthenticationRequired;
+ }
+
+ public int getUserAuthenticationValidityDurationSeconds() {
+ return userAuthenticationValidityDurationSeconds;
+ }
+
+ public boolean isStrongBoxBacked() {
+ return strongBoxBacked;
+ }
+
+ public boolean isInvalidatedByBiometricEnrollment() {
+ return invalidatedByBiometricEnrollment;
+ }
+
+ /// Android's `KeyGenParameterSpec.Builder`.
+ public static final class Builder {
+ private final String keystoreAlias;
+ private final int purposes;
+ private int keySize = -1;
+ private String[] blockModes;
+ private String[] encryptionPaddings;
+ private String[] digests;
+ private boolean randomizedEncryptionRequired = true;
+ private boolean userAuthenticationRequired;
+ private int userAuthenticationValidityDurationSeconds;
+ private boolean strongBoxBacked;
+ private boolean invalidatedByBiometricEnrollment = true;
+
+ public Builder(String keystoreAlias, int purposes) {
+ if (keystoreAlias == null) {
+ throw new NullPointerException("keystoreAlias == null");
+ }
+ if (keystoreAlias.length() == 0) {
+ throw new IllegalArgumentException("keystoreAlias must not be empty");
+ }
+ this.keystoreAlias = keystoreAlias;
+ this.purposes = purposes;
+ }
+
+ public Builder setKeySize(int keySize) {
+ if (keySize < 0) {
+ throw new IllegalArgumentException("keySize < 0");
+ }
+ this.keySize = keySize;
+ return this;
+ }
+
+ public Builder setBlockModes(String... blockModes) {
+ this.blockModes = copy(blockModes);
+ return this;
+ }
+
+ public Builder setEncryptionPaddings(String... paddings) {
+ this.encryptionPaddings = copy(paddings);
+ return this;
+ }
+
+ public Builder setDigests(String... digests) {
+ this.digests = copy(digests);
+ return this;
+ }
+
+ public Builder setRandomizedEncryptionRequired(boolean required) {
+ this.randomizedEncryptionRequired = required;
+ return this;
+ }
+
+ public Builder setUserAuthenticationRequired(boolean required) {
+ this.userAuthenticationRequired = required;
+ return this;
+ }
+
+ public Builder setUserAuthenticationValidityDurationSeconds(int seconds) {
+ this.userAuthenticationValidityDurationSeconds = seconds;
+ return this;
+ }
+
+ public Builder setUserAuthenticationParameters(int timeoutSeconds, int type) {
+ this.userAuthenticationValidityDurationSeconds = timeoutSeconds;
+ return this;
+ }
+
+ public Builder setIsStrongBoxBacked(boolean strongBox) {
+ this.strongBoxBacked = strongBox;
+ return this;
+ }
+
+ public Builder setInvalidatedByBiometricEnrollment(boolean invalidate) {
+ this.invalidatedByBiometricEnrollment = invalidate;
+ return this;
+ }
+
+ public Builder setUnlockedDeviceRequired(boolean required) {
+ return this;
+ }
+
+ public KeyGenParameterSpec build() {
+ return new KeyGenParameterSpec(this);
+ }
+ }
+}
diff --git a/maven/android-compat/src/main/java/android/security/keystore/KeyProperties.java b/maven/android-compat/src/main/java/android/security/keystore/KeyProperties.java
new file mode 100644
index 00000000000..aa755a0b51a
--- /dev/null
+++ b/maven/android-compat/src/main/java/android/security/keystore/KeyProperties.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package android.security.keystore;
+
+/// Android's `KeyProperties`: the names a key generation spec and a cipher
+/// transformation are spelled with.
+public abstract class KeyProperties {
+ public static final int PURPOSE_ENCRYPT = 1;
+ public static final int PURPOSE_DECRYPT = 2;
+ public static final int PURPOSE_SIGN = 4;
+ public static final int PURPOSE_VERIFY = 8;
+ public static final int PURPOSE_WRAP_KEY = 32;
+ public static final int PURPOSE_AGREE_KEY = 64;
+ public static final int PURPOSE_ATTEST_KEY = 128;
+
+ public static final String KEY_ALGORITHM_AES = "AES";
+ public static final String KEY_ALGORITHM_HMAC_SHA256 = "HmacSHA256";
+ public static final String KEY_ALGORITHM_HMAC_SHA512 = "HmacSHA512";
+ public static final String KEY_ALGORITHM_RSA = "RSA";
+ public static final String KEY_ALGORITHM_EC = "EC";
+
+ public static final String BLOCK_MODE_ECB = "ECB";
+ public static final String BLOCK_MODE_CBC = "CBC";
+ public static final String BLOCK_MODE_CTR = "CTR";
+ public static final String BLOCK_MODE_GCM = "GCM";
+
+ public static final String ENCRYPTION_PADDING_NONE = "NoPadding";
+ public static final String ENCRYPTION_PADDING_PKCS7 = "PKCS7Padding";
+ public static final String ENCRYPTION_PADDING_RSA_PKCS1 = "PKCS1Padding";
+ public static final String ENCRYPTION_PADDING_RSA_OAEP = "OAEPPadding";
+
+ public static final String DIGEST_NONE = "NONE";
+ public static final String DIGEST_SHA256 = "SHA-256";
+ public static final String DIGEST_SHA512 = "SHA-512";
+
+ public static final int AUTH_DEVICE_CREDENTIAL = 1;
+ public static final int AUTH_BIOMETRIC_STRONG = 2;
+
+ public static final int SECURITY_LEVEL_UNKNOWN = -2;
+ public static final int SECURITY_LEVEL_SOFTWARE = 0;
+ public static final int SECURITY_LEVEL_TRUSTED_ENVIRONMENT = 1;
+ public static final int SECURITY_LEVEL_STRONGBOX = 2;
+
+ private KeyProperties() {
+ }
+}
diff --git a/maven/android-compat/src/main/java/androidx/core/app/NotificationCompat.java b/maven/android-compat/src/main/java/androidx/core/app/NotificationCompat.java
new file mode 100644
index 00000000000..08af9482b8a
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/core/app/NotificationCompat.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.core.app;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Bundle;
+
+/// AndroidX's notification builder, over the platform one: every setter
+/// passes straight through, so what [Notification] carries over to a Codename
+/// One local notification is the same either way.
+public class NotificationCompat {
+
+ public static final int PRIORITY_DEFAULT = Notification.PRIORITY_DEFAULT;
+ public static final int PRIORITY_LOW = Notification.PRIORITY_LOW;
+ public static final int PRIORITY_MIN = Notification.PRIORITY_MIN;
+ public static final int PRIORITY_HIGH = Notification.PRIORITY_HIGH;
+ public static final int PRIORITY_MAX = Notification.PRIORITY_MAX;
+ public static final int VISIBILITY_PUBLIC = Notification.VISIBILITY_PUBLIC;
+ public static final int VISIBILITY_PRIVATE = Notification.VISIBILITY_PRIVATE;
+ public static final int VISIBILITY_SECRET = Notification.VISIBILITY_SECRET;
+ public static final int DEFAULT_ALL = Notification.DEFAULT_ALL;
+ public static final int DEFAULT_SOUND = Notification.DEFAULT_SOUND;
+ public static final int DEFAULT_VIBRATE = Notification.DEFAULT_VIBRATE;
+ public static final int DEFAULT_LIGHTS = Notification.DEFAULT_LIGHTS;
+ public static final int FLAG_ONGOING_EVENT = Notification.FLAG_ONGOING_EVENT;
+ public static final int FLAG_AUTO_CANCEL = Notification.FLAG_AUTO_CANCEL;
+ public static final int FLAG_FOREGROUND_SERVICE = Notification.FLAG_FOREGROUND_SERVICE;
+ public static final int FOREGROUND_SERVICE_DEFAULT = Notification.FOREGROUND_SERVICE_DEFAULT;
+ public static final int FOREGROUND_SERVICE_IMMEDIATE = Notification.FOREGROUND_SERVICE_IMMEDIATE;
+ public static final int FOREGROUND_SERVICE_DEFERRED = Notification.FOREGROUND_SERVICE_DEFERRED;
+ public static final String CATEGORY_SERVICE = Notification.CATEGORY_SERVICE;
+ public static final String CATEGORY_PROGRESS = Notification.CATEGORY_PROGRESS;
+ public static final String CATEGORY_NAVIGATION = Notification.CATEGORY_NAVIGATION;
+ public static final String CATEGORY_MESSAGE = Notification.CATEGORY_MESSAGE;
+ public static final String CATEGORY_ALARM = Notification.CATEGORY_ALARM;
+ public static final String CATEGORY_REMINDER = Notification.CATEGORY_REMINDER;
+ public static final String CATEGORY_STATUS = Notification.CATEGORY_STATUS;
+ public static final String CATEGORY_ERROR = Notification.CATEGORY_ERROR;
+ public static final String CATEGORY_LOCATION_SHARING = Notification.CATEGORY_LOCATION_SHARING;
+ public static final String EXTRA_TITLE = Notification.EXTRA_TITLE;
+ public static final String EXTRA_TEXT = Notification.EXTRA_TEXT;
+
+ public static Bundle getExtras(Notification notification) {
+ return notification.extras;
+ }
+
+ public static class Action extends Notification.Action {
+ public Action(int icon, CharSequence title, PendingIntent intent) {
+ super(icon, title, intent);
+ }
+ }
+
+ public abstract static class Style {
+ Notification.Style platform;
+
+ public void setBuilder(Builder builder) {
+ if (builder != null) {
+ builder.setStyle(this);
+ }
+ }
+ }
+
+ public static class BigTextStyle extends Style {
+ private final Notification.BigTextStyle style = new Notification.BigTextStyle();
+
+ public BigTextStyle() {
+ platform = style;
+ }
+
+ public BigTextStyle(Builder builder) {
+ this();
+ setBuilder(builder);
+ }
+
+ public BigTextStyle bigText(CharSequence cs) {
+ style.bigText(cs);
+ return this;
+ }
+
+ public BigTextStyle setBigContentTitle(CharSequence title) {
+ style.setBigContentTitle(title);
+ return this;
+ }
+
+ public BigTextStyle setSummaryText(CharSequence cs) {
+ return this;
+ }
+ }
+
+ public static class InboxStyle extends Style {
+ private final Notification.InboxStyle style = new Notification.InboxStyle();
+
+ public InboxStyle() {
+ platform = style;
+ }
+
+ public InboxStyle(Builder builder) {
+ this();
+ setBuilder(builder);
+ }
+
+ public InboxStyle addLine(CharSequence cs) {
+ style.addLine(cs);
+ return this;
+ }
+
+ public InboxStyle setBigContentTitle(CharSequence title) {
+ return this;
+ }
+
+ public InboxStyle setSummaryText(CharSequence cs) {
+ return this;
+ }
+ }
+
+ public static class Builder {
+ private final Notification.Builder b;
+
+ public Builder(Context context, String channelId) {
+ b = new Notification.Builder(context, channelId);
+ }
+
+ /// Deprecated in AndroidX: no channel.
+ public Builder(Context context) {
+ b = new Notification.Builder(context);
+ }
+
+ public Builder setContentTitle(CharSequence title) {
+ b.setContentTitle(title);
+ return this;
+ }
+
+ public Builder setContentText(CharSequence text) {
+ b.setContentText(text);
+ return this;
+ }
+
+ public Builder setSubText(CharSequence text) {
+ b.setSubText(text);
+ return this;
+ }
+
+ public Builder setSmallIcon(int icon) {
+ b.setSmallIcon(icon);
+ return this;
+ }
+
+ public Builder setLargeIcon(android.graphics.Bitmap icon) {
+ b.setLargeIcon(icon);
+ return this;
+ }
+
+ public Builder setTicker(CharSequence text) {
+ b.setTicker(text);
+ return this;
+ }
+
+ public Builder setWhen(long when) {
+ b.setWhen(when);
+ return this;
+ }
+
+ public Builder setShowWhen(boolean show) {
+ b.setShowWhen(show);
+ return this;
+ }
+
+ public Builder setUsesChronometer(boolean u) {
+ b.setUsesChronometer(u);
+ return this;
+ }
+
+ public Builder setNumber(int number) {
+ b.setNumber(number);
+ return this;
+ }
+
+ public Builder setContentIntent(PendingIntent intent) {
+ b.setContentIntent(intent);
+ return this;
+ }
+
+ public Builder setDeleteIntent(PendingIntent intent) {
+ b.setDeleteIntent(intent);
+ return this;
+ }
+
+ public Builder setFullScreenIntent(PendingIntent intent, boolean highPriority) {
+ b.setFullScreenIntent(intent, highPriority);
+ return this;
+ }
+
+ public Builder setOngoing(boolean ongoing) {
+ b.setOngoing(ongoing);
+ return this;
+ }
+
+ public Builder setAutoCancel(boolean autoCancel) {
+ b.setAutoCancel(autoCancel);
+ return this;
+ }
+
+ public Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
+ b.setOnlyAlertOnce(onlyAlertOnce);
+ return this;
+ }
+
+ public Builder setSilent(boolean silent) {
+ return this;
+ }
+
+ public Builder setPriority(int pri) {
+ b.setPriority(pri);
+ return this;
+ }
+
+ public Builder setCategory(String category) {
+ b.setCategory(category);
+ return this;
+ }
+
+ public Builder setVisibility(int visibility) {
+ b.setVisibility(visibility);
+ return this;
+ }
+
+ public Builder setColor(int argb) {
+ b.setColor(argb);
+ return this;
+ }
+
+ public Builder setColorized(boolean colorize) {
+ b.setColorized(colorize);
+ return this;
+ }
+
+ public Builder setDefaults(int defaults) {
+ b.setDefaults(defaults);
+ return this;
+ }
+
+ public Builder setSound(android.net.Uri sound) {
+ b.setSound(sound);
+ return this;
+ }
+
+ public Builder setVibrate(long[] pattern) {
+ b.setVibrate(pattern);
+ return this;
+ }
+
+ public Builder setLights(int argb, int onMs, int offMs) {
+ b.setLights(argb, onMs, offMs);
+ return this;
+ }
+
+ public Builder setGroup(String groupKey) {
+ b.setGroup(groupKey);
+ return this;
+ }
+
+ public Builder setGroupSummary(boolean isGroupSummary) {
+ b.setGroupSummary(isGroupSummary);
+ return this;
+ }
+
+ public Builder setSortKey(String sortKey) {
+ b.setSortKey(sortKey);
+ return this;
+ }
+
+ public Builder setChannelId(String channelId) {
+ b.setChannelId(channelId);
+ return this;
+ }
+
+ public Builder setTimeoutAfter(long durationMs) {
+ b.setTimeoutAfter(durationMs);
+ return this;
+ }
+
+ public Builder setProgress(int max, int progress, boolean indeterminate) {
+ b.setProgress(max, progress, indeterminate);
+ return this;
+ }
+
+ public Builder setForegroundServiceBehavior(int behavior) {
+ b.setForegroundServiceBehavior(behavior);
+ return this;
+ }
+
+ public Builder setLocalOnly(boolean localOnly) {
+ b.setLocalOnly(localOnly);
+ return this;
+ }
+
+ public Builder setStyle(Style style) {
+ b.setStyle(style == null ? null : style.platform);
+ return this;
+ }
+
+ public Builder addAction(int icon, CharSequence title, PendingIntent intent) {
+ b.addAction(icon, title, intent);
+ return this;
+ }
+
+ public Builder addAction(Action action) {
+ b.addAction(action);
+ return this;
+ }
+
+ public Builder setExtras(Bundle extras) {
+ b.setExtras(extras);
+ return this;
+ }
+
+ public Bundle getExtras() {
+ return b.getExtras();
+ }
+
+ public Notification build() {
+ return b.build();
+ }
+ }
+}
diff --git a/maven/android-compat/src/main/java/androidx/core/app/NotificationManagerCompat.java b/maven/android-compat/src/main/java/androidx/core/app/NotificationManagerCompat.java
new file mode 100644
index 00000000000..3bf2898b90a
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/core/app/NotificationManagerCompat.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.core.app;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+
+import java.util.List;
+
+public final class NotificationManagerCompat {
+
+ public static final int IMPORTANCE_UNSPECIFIED = NotificationManager.IMPORTANCE_UNSPECIFIED;
+ public static final int IMPORTANCE_NONE = NotificationManager.IMPORTANCE_NONE;
+ public static final int IMPORTANCE_MIN = NotificationManager.IMPORTANCE_MIN;
+ public static final int IMPORTANCE_LOW = NotificationManager.IMPORTANCE_LOW;
+ public static final int IMPORTANCE_DEFAULT = NotificationManager.IMPORTANCE_DEFAULT;
+ public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH;
+ public static final int IMPORTANCE_MAX = NotificationManager.IMPORTANCE_MAX;
+
+ private final NotificationManager manager;
+
+ private NotificationManagerCompat(NotificationManager manager) {
+ this.manager = manager;
+ }
+
+ public static NotificationManagerCompat from(Context context) {
+ return new NotificationManagerCompat(NotificationManager.getInstance());
+ }
+
+ public boolean areNotificationsEnabled() {
+ return manager.areNotificationsEnabled();
+ }
+
+ public void notify(int id, Notification notification) {
+ manager.notify(id, notification);
+ }
+
+ public void notify(String tag, int id, Notification notification) {
+ manager.notify(tag, id, notification);
+ }
+
+ public void cancel(int id) {
+ manager.cancel(id);
+ }
+
+ public void cancel(String tag, int id) {
+ manager.cancel(tag, id);
+ }
+
+ public void cancelAll() {
+ manager.cancelAll();
+ }
+
+ public void createNotificationChannel(NotificationChannel channel) {
+ manager.createNotificationChannel(channel);
+ }
+
+ public void createNotificationChannels(List channels) {
+ manager.createNotificationChannels(channels);
+ }
+
+ public NotificationChannel getNotificationChannel(String channelId) {
+ return manager.getNotificationChannel(channelId);
+ }
+
+ public void deleteNotificationChannel(String channelId) {
+ manager.deleteNotificationChannel(channelId);
+ }
+}
diff --git a/maven/android-compat/src/main/java/androidx/core/app/ServiceCompat.java b/maven/android-compat/src/main/java/androidx/core/app/ServiceCompat.java
new file mode 100644
index 00000000000..95a2a922cde
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/core/app/ServiceCompat.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.core.app;
+
+import android.app.Notification;
+import android.app.Service;
+
+public final class ServiceCompat {
+
+ public static final int START_STICKY = Service.START_STICKY;
+ public static final int STOP_FOREGROUND_REMOVE = Service.STOP_FOREGROUND_REMOVE;
+ public static final int STOP_FOREGROUND_DETACH = Service.STOP_FOREGROUND_DETACH;
+
+ private ServiceCompat() {
+ }
+
+ public static void startForeground(Service service, int id, Notification notification,
+ int foregroundServiceType) {
+ service.startForeground(id, notification, foregroundServiceType);
+ }
+
+ public static void stopForeground(Service service, int flags) {
+ service.stopForeground(flags);
+ }
+}
diff --git a/maven/android-compat/src/main/java/androidx/core/content/ContextCompat.java b/maven/android-compat/src/main/java/androidx/core/content/ContextCompat.java
index 880ab59dab7..24234118e3f 100644
--- a/maven/android-compat/src/main/java/androidx/core/content/ContextCompat.java
+++ b/maven/android-compat/src/main/java/androidx/core/content/ContextCompat.java
@@ -49,6 +49,10 @@ public static Drawable getDrawable(Context context, int id) {
return context.getDrawable(id);
}
+ public static void startForegroundService(Context context, Intent intent) {
+ context.startForegroundService(intent);
+ }
+
public static int checkSelfPermission(Context context, String permission) {
return context.checkSelfPermission(permission);
}
diff --git a/maven/android-compat/src/main/java/androidx/room/AutoMigration.java b/maven/android-compat/src/main/java/androidx/room/AutoMigration.java
new file mode 100644
index 00000000000..2b2772d603b
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/AutoMigration.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Declared for source compatibility; an automatic migration fails the build.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({})
+@Retention(RetentionPolicy.CLASS)
+public @interface AutoMigration {
+ int from();
+ int to();
+ Class> spec() default Object.class;
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/BuiltInTypeConverters.java b/maven/android-compat/src/main/java/androidx/room/BuiltInTypeConverters.java
new file mode 100644
index 00000000000..2b8d41777f5
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/BuiltInTypeConverters.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Whether the built-in converters (enums) apply.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({})
+@Retention(RetentionPolicy.CLASS)
+public @interface BuiltInTypeConverters {
+ State enums() default State.INHERITED;
+ State uuid() default State.INHERITED;
+
+ /// On, off, or as the enclosing scope says.
+ enum State {
+ ENABLED, DISABLED, INHERITED
+ }
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/ColumnInfo.java b/maven/android-compat/src/main/java/androidx/room/ColumnInfo.java
new file mode 100644
index 00000000000..1ebc9d6da92
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/ColumnInfo.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Names and types a column.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface ColumnInfo {
+ String INHERIT_FIELD_NAME = "[field-name]";
+ int UNDEFINED = 1;
+ int TEXT = 2;
+ int INTEGER = 3;
+ int REAL = 4;
+ int BLOB = 5;
+ int UNSPECIFIED = 1;
+ int BINARY = 2;
+ int NOCASE = 3;
+ int RTRIM = 4;
+ int LOCALIZED = 5;
+ int UNICODE = 6;
+ String VALUE_UNSPECIFIED = "[value-unspecified]";
+
+ String name() default INHERIT_FIELD_NAME;
+ int typeAffinity() default UNDEFINED;
+ boolean index() default false;
+ int collate() default UNSPECIFIED;
+ String defaultValue() default VALUE_UNSPECIFIED;
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Dao.java b/maven/android-compat/src/main/java/androidx/room/Dao.java
new file mode 100644
index 00000000000..32f5a886741
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Dao.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Marks an interface or abstract class whose implementation the build generates.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface Dao {
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Database.java b/maven/android-compat/src/main/java/androidx/room/Database.java
new file mode 100644
index 00000000000..6a4f22cb614
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Database.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Marks the abstract database class; the build generates its implementation.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface Database {
+ Class>[] entities();
+ Class>[] views() default {};
+ int version();
+ boolean exportSchema() default true;
+ AutoMigration[] autoMigrations() default {};
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/DatabaseView.java b/maven/android-compat/src/main/java/androidx/room/DatabaseView.java
new file mode 100644
index 00000000000..9ef0bf7de94
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/DatabaseView.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// A view; the Codename One Room build refuses it, naming the class.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface DatabaseView {
+ String value() default "";
+ String viewName() default "";
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Delete.java b/maven/android-compat/src/main/java/androidx/room/Delete.java
new file mode 100644
index 00000000000..c4273833948
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Delete.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Deletes the entities a DAO method is passed, by primary key.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Delete {
+ Class> entity() default Object.class;
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Embedded.java b/maven/android-compat/src/main/java/androidx/room/Embedded.java
new file mode 100644
index 00000000000..6056eb5e54a
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Embedded.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Stores an object's fields as columns of the enclosing entity.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Embedded {
+ String prefix() default "";
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Entity.java b/maven/android-compat/src/main/java/androidx/room/Entity.java
new file mode 100644
index 00000000000..3a403711ea1
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Entity.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Marks a class as a Room table. The build generates its table, column mapping and indices.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface Entity {
+ String tableName() default "";
+ Index[] indices() default {};
+ boolean inheritSuperIndices() default false;
+ String[] primaryKeys() default {};
+ ForeignKey[] foreignKeys() default {};
+ String[] ignoredColumns() default {};
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/ForeignKey.java b/maven/android-compat/src/main/java/androidx/room/ForeignKey.java
new file mode 100644
index 00000000000..05fdd24aa20
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/ForeignKey.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// A foreign key of an entity's table.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({})
+@Retention(RetentionPolicy.CLASS)
+public @interface ForeignKey {
+ int NO_ACTION = 1;
+ int RESTRICT = 2;
+ int SET_NULL = 3;
+ int SET_DEFAULT = 4;
+ int CASCADE = 5;
+
+ Class> entity();
+ String[] parentColumns();
+ String[] childColumns();
+ int onDelete() default NO_ACTION;
+ int onUpdate() default NO_ACTION;
+ boolean deferred() default false;
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Fts3.java b/maven/android-compat/src/main/java/androidx/room/Fts3.java
new file mode 100644
index 00000000000..e5aa9105823
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Fts3.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// A full-text entity; the Codename One Room build refuses it, naming the class.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface Fts3 {
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Fts4.java b/maven/android-compat/src/main/java/androidx/room/Fts4.java
new file mode 100644
index 00000000000..354179b1f40
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Fts4.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// A full-text entity; the Codename One Room build refuses it, naming the class.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface Fts4 {
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Ignore.java b/maven/android-compat/src/main/java/androidx/room/Ignore.java
new file mode 100644
index 00000000000..76e8a6c0bc2
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Ignore.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Leaves a field or constructor out of the mapping.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.CLASS)
+public @interface Ignore {
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Index.java b/maven/android-compat/src/main/java/androidx/room/Index.java
new file mode 100644
index 00000000000..5fb9a9bafaf
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Index.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// An index of an entity's table.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({})
+@Retention(RetentionPolicy.CLASS)
+public @interface Index {
+ String[] value();
+ String name() default "";
+ boolean unique() default false;
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Insert.java b/maven/android-compat/src/main/java/androidx/room/Insert.java
new file mode 100644
index 00000000000..b5f7a3784d8
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Insert.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Inserts the entities a DAO method is passed.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Insert {
+ Class> entity() default Object.class;
+ int onConflict() default OnConflictStrategy.ABORT;
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/InvalidationTracker.java b/maven/android-compat/src/main/java/androidx/room/InvalidationTracker.java
new file mode 100644
index 00000000000..e4e0bce8c39
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/InvalidationTracker.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import androidx.lifecycle.LiveData;
+
+import com.codename1.androidcompat.runtime.MainThread;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+/// Tells observers which tables changed. Room on Android learns this from
+/// triggers; here the generated DAOs report every table a write names, and a
+/// change made inside a transaction is reported once the outermost
+/// transaction ends. A statement the application runs itself through
+/// `getOpenHelper()` is not seen, as no trigger watches it.
+public class InvalidationTracker {
+
+ private final RoomDatabase mDatabase;
+ private final String[] mTableNames;
+ private final List mObservers = new ArrayList();
+ /// Tables written by the current thread and not yet reported. Per
+ /// thread because a transaction is: its writes are reported when that
+ /// thread's outermost transaction ends, never by another thread.
+ private final ThreadLocal> mPending = new ThreadLocal>();
+
+ /// An observer of a set of tables, named case-insensitively.
+ public abstract static class Observer {
+ final String[] mTables;
+
+ protected Observer(String firstTable, String... rest) {
+ this(join(firstTable, rest));
+ }
+
+ public Observer(String[] tables) {
+ mTables = new String[tables.length];
+ for (int i = 0; i < tables.length; i++) {
+ mTables[i] = asciiLower(tables[i]);
+ }
+ }
+
+ public abstract void onInvalidated(Set tables);
+
+ boolean isRemote() {
+ return false;
+ }
+
+ private static String[] join(String first, String[] rest) {
+ String[] all = new String[rest.length + 1];
+ all[0] = first;
+ System.arraycopy(rest, 0, all, 1, rest.length);
+ return all;
+ }
+ }
+
+ public InvalidationTracker(RoomDatabase database, String... tableNames) {
+ mDatabase = database;
+ mTableNames = new String[tableNames.length];
+ for (int i = 0; i < tableNames.length; i++) {
+ mTableNames[i] = asciiLower(tableNames[i]);
+ }
+ }
+
+ /// Call on the main thread, as `LiveData` does from `onActive`; observers
+ /// are notified there.
+ public void addObserver(Observer observer) {
+ for (String t : observer.mTables) {
+ if (!knows(t)) {
+ throw new IllegalArgumentException("There is no table with name " + t);
+ }
+ }
+ if (!mObservers.contains(observer)) {
+ mObservers.add(observer);
+ }
+ }
+
+ public void removeObserver(Observer observer) {
+ mObservers.remove(observer);
+ }
+
+ /// Delivers what is pending. Room schedules this after a write; here a
+ /// write outside a transaction is delivered at once.
+ public void refreshVersionsAsync() {
+ flush();
+ }
+
+ public void refreshVersionsSync() {
+ flush();
+ }
+
+ /// Records that the named tables changed. Generated DAOs call it after
+ /// every write.
+ public void notifyTablesChanged(String... tables) {
+ Set pending = mPending.get();
+ if (pending == null) {
+ pending = new HashSet();
+ mPending.set(pending);
+ }
+ for (String t : tables) {
+ pending.add(asciiLower(t));
+ }
+ if (!mDatabase.inTransaction()) {
+ flush();
+ }
+ }
+
+ /// Reports the current thread's pending tables. Observers are called on
+ /// the main thread, which is the only thread that changes the observer
+ /// list (LiveData registers from `onActive`), so a writer on a worker
+ /// thread posts the report instead of walking the list itself.
+ void flush() {
+ Set pending = mPending.get();
+ if (pending == null || pending.isEmpty()) {
+ return;
+ }
+ final Set changed = new HashSet(pending);
+ pending.clear();
+ if (MainThread.isMainThread()) {
+ dispatch(changed);
+ } else {
+ MainThread.post(new Runnable() {
+ @Override
+ public void run() {
+ dispatch(changed);
+ }
+ });
+ }
+ }
+
+ private void dispatch(Set changed) {
+ List copy = new ArrayList(mObservers);
+ for (Observer o : copy) {
+ if (!mObservers.contains(o)) {
+ // Removed by an earlier observer's callback in this pass.
+ continue;
+ }
+ Set hit = null;
+ for (String t : o.mTables) {
+ if (changed.contains(t)) {
+ if (hit == null) {
+ hit = new HashSet();
+ }
+ hit.add(t);
+ }
+ }
+ if (hit != null) {
+ o.onInvalidated(Collections.unmodifiableSet(hit));
+ }
+ }
+ }
+
+ /// A live query: computed when it gains an active observer and again
+ /// after every change to `tableNames` while it has one.
+ public LiveData createLiveData(String[] tableNames, boolean inTransaction, Callable computeFunction) {
+ return new RoomTrackingLiveData(mDatabase, tableNames, inTransaction, computeFunction);
+ }
+
+ public LiveData createLiveData(String[] tableNames, Callable computeFunction) {
+ return createLiveData(tableNames, false, computeFunction);
+ }
+
+ private boolean knows(String table) {
+ for (String t : mTableNames) {
+ if (t.equals(table)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// ASCII-only lower case: `toLowerCase()` follows the device locale.
+ static String asciiLower(String s) {
+ char[] out = null;
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c >= 'A' && c <= 'Z') {
+ if (out == null) {
+ out = s.toCharArray();
+ }
+ out[i] = (char) (c + ('a' - 'A'));
+ }
+ }
+ return out == null ? s : new String(out);
+ }
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Junction.java b/maven/android-compat/src/main/java/androidx/room/Junction.java
new file mode 100644
index 00000000000..874d05fc23b
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Junction.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// The cross-reference table of a many-to-many [Relation].
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({})
+@Retention(RetentionPolicy.CLASS)
+public @interface Junction {
+ Class> value();
+ String parentColumn() default "";
+ String entityColumn() default "";
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/OnConflictStrategy.java b/maven/android-compat/src/main/java/androidx/room/OnConflictStrategy.java
new file mode 100644
index 00000000000..75e030f34de
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/OnConflictStrategy.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+/// What an insert or update does when it breaks a constraint.
+public final class OnConflictStrategy {
+
+ public static final int NONE = 0;
+ public static final int REPLACE = 1;
+ public static final int ROLLBACK = 2;
+ public static final int ABORT = 3;
+ public static final int FAIL = 4;
+ public static final int IGNORE = 5;
+
+ private OnConflictStrategy() {
+ }
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/PrimaryKey.java b/maven/android-compat/src/main/java/androidx/room/PrimaryKey.java
new file mode 100644
index 00000000000..1407fc86c5e
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/PrimaryKey.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// The primary key column of an entity.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface PrimaryKey {
+ boolean autoGenerate() default false;
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/ProvidedTypeConverter.java b/maven/android-compat/src/main/java/androidx/room/ProvidedTypeConverter.java
new file mode 100644
index 00000000000..ce8b084db47
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/ProvidedTypeConverter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// A converter whose instance the application supplies through `Builder.addTypeConverter`.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface ProvidedTypeConverter {
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Query.java b/maven/android-compat/src/main/java/androidx/room/Query.java
new file mode 100644
index 00000000000..a6d4a626347
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Query.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// A SQL statement run by a DAO method; `:name` binds a parameter.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Query {
+ String value();
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/RawQuery.java b/maven/android-compat/src/main/java/androidx/room/RawQuery.java
new file mode 100644
index 00000000000..70353204e2a
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/RawQuery.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// A DAO method that runs the query it is passed.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface RawQuery {
+ Class>[] observedEntities() default {};
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Relation.java b/maven/android-compat/src/main/java/androidx/room/Relation.java
new file mode 100644
index 00000000000..940e5ddc13a
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Relation.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// A relation between entities; the Codename One Room build refuses it, naming the field.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Relation {
+ Class> entity() default Object.class;
+ String parentColumn();
+ String entityColumn();
+ Junction associateBy() default @Junction(Object.class);
+ String[] projection() default {};
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/RewriteQueriesToDropUnusedColumns.java b/maven/android-compat/src/main/java/androidx/room/RewriteQueriesToDropUnusedColumns.java
new file mode 100644
index 00000000000..1afb364a554
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/RewriteQueriesToDropUnusedColumns.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/// Accepted and ignored.
+///
+/// Kept in the class files: the Room step of the Android remap reads it there
+/// and writes the code AndroidX Room's annotation processor would.
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface RewriteQueriesToDropUnusedColumns {
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/Room.java b/maven/android-compat/src/main/java/androidx/room/Room.java
new file mode 100644
index 00000000000..4bdf7a1b215
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/Room.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import android.content.Context;
+
+/// Creates Room databases.
+public final class Room {
+
+ public static final String MASTER_TABLE_NAME = RoomDatabase.MASTER_TABLE;
+
+ private Room() {
+ }
+
+ /// A database stored under `name`.
+ public static RoomDatabase.Builder databaseBuilder(Context context, Class klass,
+ String name) {
+ if (name == null || name.trim().length() == 0) {
+ throw new IllegalArgumentException("Cannot build a database with null or empty name."
+ + " If you are trying to create an in memory database, use Room.inMemoryDatabaseBuilder");
+ }
+ return new RoomDatabase.Builder(context, klass, name);
+ }
+
+ /// A database that lives until it is closed.
+ public static RoomDatabase.Builder inMemoryDatabaseBuilder(Context context,
+ Class klass) {
+ return new RoomDatabase.Builder(context, klass, null);
+ }
+}
diff --git a/maven/android-compat/src/main/java/androidx/room/RoomDatabase.java b/maven/android-compat/src/main/java/androidx/room/RoomDatabase.java
new file mode 100644
index 00000000000..d4805051dbe
--- /dev/null
+++ b/maven/android-compat/src/main/java/androidx/room/RoomDatabase.java
@@ -0,0 +1,656 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.room;
+
+import android.content.Context;
+import android.database.Cursor;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteStatement;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+import com.codename1.androidcompat.runtime.MainThread;
+import com.codename1.androidcompat.runtime.RoomFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+
+/// The base of an application's `@Database` class. The build generates the
+/// subclass (`Name_Impl`) that knows the schema; this class opens the
+/// database, creates the tables on the first open, runs the migrations a
+/// version change needs and checks that the schema did not change without
+/// one, as Room does.
+public abstract class RoomDatabase {
+
+ public static final int MAX_BIND_PARAMETER_CNT = 999;
+ static final String MASTER_TABLE = "room_master_table";
+ private static final int MASTER_ID = 42;
+
+ private SupportSQLiteOpenHelper mOpenHelper;
+ private InvalidationTracker mInvalidationTracker;
+ private boolean mAllowMainThreadQueries;
+ private Executor mQueryExecutor;
+ private Executor mTransactionExecutor;
+ private final Map, Object> mTypeConverters = new HashMap, Object>();
+ private List mCallbacks;
+ private MigrationContainer mMigrations;
+ private boolean mDestructive;
+ private boolean mDestructiveOnDowngrade;
+ private Set mDestructiveFrom;
+ private boolean mOpen;
+ /// Live queries recompute on the main thread when that is where the
+ /// change happened; they are exempt from the main-thread check, which
+ /// is there for the application's own calls. Touched on the main thread
+ /// only.
+
+ public RoomDatabase() {
+ }
+
+ // ---------------------------------------------------------- generated
+
+ /// The `version` of the `@Database` annotation.
+ protected abstract int roomSchemaVersion();
+
+ /// The statements creating every table and index, in order.
+ protected abstract String[] roomCreateStatements();
+
+ /// The entity tables.
+ protected abstract String[] roomTableNames();
+
+ /// Each entity table followed by its columns, which a migration must
+ /// leave in place; null skips the check.
+ protected String[][] roomTableColumns() {
+ return null;
+ }
+
+ /// A new instance of a `@TypeConverters` class the application did not
+ /// supply, or null for one it must (`@ProvidedTypeConverter`).
+ protected Object roomCreateTypeConverter(Class> converterClass) {
+ return null;
+ }
+
+ // ---------------------------------------------------------- lifecycle
+
+ void init(Builder> b) {
+ mAllowMainThreadQueries = b.mAllowMainThreadQueries;
+ mQueryExecutor = b.mQueryExecutor;
+ mTransactionExecutor = b.mTransactionExecutor != null ? b.mTransactionExecutor : b.mQueryExecutor;
+ mCallbacks = b.mCallbacks;
+ mMigrations = b.mMigrationContainer;
+ mDestructive = b.mDestructive;
+ mDestructiveOnDowngrade = b.mDestructiveOnDowngrade;
+ mDestructiveFrom = b.mDestructiveFrom;
+ for (Object c : b.mTypeConverters) {
+ mTypeConverters.put(c.getClass(), c);
+ }
+ mInvalidationTracker = new InvalidationTracker(this, roomTableNames());
+ SupportSQLiteOpenHelper.Factory factory = b.mFactory != null ? b.mFactory
+ : new FrameworkSQLiteOpenHelperFactory();
+ mOpenHelper = factory.create(new SupportSQLiteOpenHelper.Configuration(b.mContext, b.mName,
+ new OpenCallback(roomSchemaVersion())));
+ mOpen = true;
+ }
+
+ public SupportSQLiteOpenHelper getOpenHelper() {
+ return mOpenHelper;
+ }
+
+ public InvalidationTracker getInvalidationTracker() {
+ return mInvalidationTracker;
+ }
+
+ public Executor getQueryExecutor() {
+ return mQueryExecutor;
+ }
+
+ public Executor getTransactionExecutor() {
+ return mTransactionExecutor;
+ }
+
+ public boolean isOpen() {
+ return mOpen;
+ }
+
+ public void close() {
+ if (mOpen) {
+ mOpen = false;
+ mOpenHelper.close();
+ }
+ }
+
+ /// The instance of a `@TypeConverters` class: the one the builder was
+ /// given, or one created for it.
+ @SuppressWarnings("unchecked")
+ public T getTypeConverter(Class klass) {
+ Object c = mTypeConverters.get(klass);
+ if (c == null) {
+ c = roomCreateTypeConverter(klass);
+ if (c == null) {
+ throw new IllegalArgumentException("A required type converter (" + klass.getName()
+ + ") for " + getClass().getName() + " is missing in the database configuration.");
+ }
+ mTypeConverters.put(klass, c);
+ }
+ return (T) c;
+ }
+
+ // ---------------------------------------------------------- access
+
+ /// Throws on the main thread unless `allowMainThreadQueries()` was set.
+ public void assertNotMainThread() {
+ if (mAllowMainThreadQueries) {
+ return;
+ }
+ if (MainThread.isMainThread() && com.codename1.ui.Display.isInitialized()) {
+ throw new IllegalStateException("Cannot access database on the main thread since"
+ + " it may potentially lock the UI for a long period of time.");
+ }
+ }
+
+ public Cursor query(String query, Object[] args) {
+ return query(new SimpleSQLiteQuery(query, args));
+ }
+
+ public Cursor query(SupportSQLiteQuery query) {
+ assertNotMainThread();
+ return mOpenHelper.getWritableDatabase().query(query);
+ }
+
+ public SupportSQLiteStatement compileStatement(String sql) {
+ assertNotMainThread();
+ return mOpenHelper.getWritableDatabase().compileStatement(sql);
+ }
+
+ public void beginTransaction() {
+ assertNotMainThread();
+ mOpenHelper.getWritableDatabase().beginTransaction();
+ }
+
+ public void endTransaction() {
+ SupportSQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.endTransaction();
+ if (!db.inTransaction()) {
+ mInvalidationTracker.flush();
+ }
+ }
+
+ public void setTransactionSuccessful() {
+ mOpenHelper.getWritableDatabase().setTransactionSuccessful();
+ }
+
+ public boolean inTransaction() {
+ return mOpen && mOpenHelper.getWritableDatabase().inTransaction();
+ }
+
+ public void runInTransaction(Runnable body) {
+ beginTransaction();
+ try {
+ body.run();
+ setTransactionSuccessful();
+ } finally {
+ endTransaction();
+ }
+ }
+
+ public V runInTransaction(Callable body) {
+ beginTransaction();
+ try {
+ V result = body.call();
+ setTransactionSuccessful();
+ return result;
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ RoomDatabase.sneakyThrow(e);
+ return null;
+ } finally {
+ endTransaction();
+ }
+ }
+
+ /// Rethrows a checked exception unwrapped, as Room's `SneakyThrow` does.
+ @SuppressWarnings("unchecked")
+ private static void sneakyThrow(Throwable t) throws E {
+ throw (E) t;
+ }
+
+ /// Deletes every row of every entity table, foreign keys deferred.
+ public void clearAllTables() {
+ assertNotMainThread();
+ SupportSQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ boolean fk = hasForeignKeys();
+ try {
+ if (fk) {
+ db.execSQL("PRAGMA defer_foreign_keys = TRUE");
+ }
+ db.beginTransaction();
+ try {
+ for (String t : roomTableNames()) {
+ db.execSQL("DELETE FROM `" + t + "`");
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ } finally {
+ if (!db.inTransaction()) {
+ db.execSQL("VACUUM");
+ }
+ }
+ mInvalidationTracker.notifyTablesChanged(roomTableNames());
+ }
+
+ // ---------------------------------------------------------- schema
+
+ private boolean hasForeignKeys() {
+ for (String s : roomCreateStatements()) {
+ if (s.indexOf(" REFERENCES ") >= 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// The schema's fingerprint, stored in `room_master_table`. A database
+ /// opened at its version with another fingerprint changed its schema
+ /// without a version bump.
+ String identityHash() {
+ StringBuilder all = new StringBuilder();
+ for (String s : roomCreateStatements()) {
+ all.append(s).append('\n');
+ }
+ String text = all.toString();
+ long h = 1125899906842597L;
+ for (int i = 0; i < text.length(); i++) {
+ h = 31 * h + text.charAt(i);
+ }
+ String hex = Long.toHexString(h);
+ while (hex.length() < 16) {
+ hex = "0" + hex;
+ }
+ return hex + Integer.toHexString(text.length());
+ }
+
+ void createAllTables(SupportSQLiteDatabase db) {
+ for (String s : roomCreateStatements()) {
+ db.execSQL(s);
+ }
+ writeIdentity(db);
+ }
+
+ private void writeIdentity(SupportSQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + MASTER_TABLE + " (id INTEGER PRIMARY KEY,identity_hash TEXT)");
+ db.execSQL("INSERT OR REPLACE INTO " + MASTER_TABLE + " (id,identity_hash) VALUES(" + MASTER_ID + ", '"
+ + identityHash() + "')");
+ }
+
+ private void dropAllTables(SupportSQLiteDatabase db) {
+ for (String t : roomTableNames()) {
+ db.execSQL("DROP TABLE IF EXISTS `" + t + "`");
+ }
+ db.execSQL("DROP TABLE IF EXISTS " + MASTER_TABLE);
+ }
+
+ void migrate(SupportSQLiteDatabase db, int from, int to) {
+ List path = mMigrations.findMigrationPath(from, to);
+ if (path != null) {
+ for (Migration m : path) {
+ m.migrate(db);
+ }
+ validateSchema(db);
+ writeIdentity(db);
+ return;
+ }
+ boolean downgrade = from > to;
+ boolean allowed = mDestructive || (downgrade && mDestructiveOnDowngrade)
+ || (mDestructiveFrom != null && mDestructiveFrom.contains(Integer.valueOf(from)));
+ if (!allowed) {
+ throw new IllegalStateException("A migration from " + from + " to " + to + " was required but not"
+ + " found. Please provide the necessary Migration path via"
+ + " RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via"
+ + " one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
+ }
+ dropAllTables(db);
+ if (mCallbacks != null) {
+ for (Callback c : mCallbacks) {
+ c.onDestructiveMigration(db);
+ }
+ }
+ createAllTables(db);
+ }
+
+ /// Room refuses a migration that left a table without the columns its
+ /// entity declares, rather than stamping the database valid and failing
+ /// at the first query. This compares column names; types, nullability and
+ /// indices are not checked.
+ private void validateSchema(SupportSQLiteDatabase db) {
+ String[][] expected = roomTableColumns();
+ if (expected == null) {
+ return;
+ }
+ for (String[] table : expected) {
+ java.util.Set want = new java.util.TreeSet();
+ for (int i = 1; i < table.length; i++) {
+ want.add(InvalidationTracker.asciiLower(table[i]));
+ }
+ java.util.Set found = new java.util.TreeSet();
+ Cursor c = db.query("PRAGMA table_info(`" + table[0] + "`)");
+ try {
+ int name = c.getColumnIndex("name");
+ while (c.moveToNext()) {
+ found.add(InvalidationTracker.asciiLower(c.getString(name)));
+ }
+ } finally {
+ c.close();
+ }
+ if (!found.equals(want)) {
+ throw new IllegalStateException("Migration didn't properly handle: " + table[0]
+ + ".\n Expected columns: " + want + "\n Found columns: " + found);
+ }
+ }
+ }
+
+ void checkIdentity(SupportSQLiteDatabase db) {
+ Cursor c = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='" + MASTER_TABLE + "'");
+ boolean hasMaster;
+ try {
+ hasMaster = c.moveToFirst();
+ } finally {
+ c.close();
+ }
+ if (!hasMaster) {
+ writeIdentity(db);
+ return;
+ }
+ String found = null;
+ c = db.query("SELECT identity_hash FROM " + MASTER_TABLE + " WHERE id = " + MASTER_ID + " LIMIT 1");
+ try {
+ if (c.moveToFirst()) {
+ found = c.getString(0);
+ }
+ } finally {
+ c.close();
+ }
+ String expected = identityHash();
+ if (!expected.equals(found)) {
+ throw new IllegalStateException("Room cannot verify the data integrity. Looks like you've changed"
+ + " schema but forgot to update the version number. You can simply fix this by increasing"
+ + " the version number. Expected identity hash: " + expected + ", found: " + found);
+ }
+ }
+
+ /// The open hooks: create, migrate, verify.
+ private final class OpenCallback extends SupportSQLiteOpenHelper.Callback {
+ OpenCallback(int version) {
+ super(version);
+ }
+
+ @Override
+ public void onCreate(SupportSQLiteDatabase db) {
+ createAllTables(db);
+ if (mCallbacks != null) {
+ for (Callback c : mCallbacks) {
+ c.onCreate(db);
+ }
+ }
+ }
+
+ @Override
+ public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+ migrate(db, oldVersion, newVersion);
+ }
+
+ @Override
+ public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+ migrate(db, oldVersion, newVersion);
+ }
+
+ @Override
+ public void onOpen(SupportSQLiteDatabase db) {
+ checkIdentity(db);
+ if (hasForeignKeys()) {
+ db.execSQL("PRAGMA foreign_keys = ON");
+ }
+ if (mCallbacks != null) {
+ for (Callback c : mCallbacks) {
+ c.onOpen(db);
+ }
+ }
+ }
+ }
+
+ // ---------------------------------------------------------- nested
+
+ /// Hooks an application adds with `Builder.addCallback`.
+ public abstract static class Callback {
+ public void onCreate(SupportSQLiteDatabase db) {
+ }
+
+ public void onOpen(SupportSQLiteDatabase db) {
+ }
+
+ public void onDestructiveMigration(SupportSQLiteDatabase db) {
+ }
+ }
+
+ /// The journal mode; accepted, and left to the platform.
+ public enum JournalMode {
+ AUTOMATIC, TRUNCATE, WRITE_AHEAD_LOGGING
+ }
+
+ /// The migrations a builder was given, by start and end version.
+ public static class MigrationContainer {
+ private final Map> mMigrations =
+ new HashMap>();
+
+ public void addMigrations(Migration... migrations) {
+ for (Migration m : migrations) {
+ addMigration(m);
+ }
+ }
+
+ void addMigration(Migration m) {
+ Integer start = Integer.valueOf(m.startVersion);
+ Map ends = mMigrations.get(start);
+ if (ends == null) {
+ ends = new HashMap();
+ mMigrations.put(start, ends);
+ }
+ ends.put(Integer.valueOf(m.endVersion), m);
+ }
+
+ /// Room's walk: from each version, the migration that goes furthest
+ /// toward `end` without passing it.
+ public List findMigrationPath(int start, int end) {
+ if (start == end) {
+ return new ArrayList();
+ }
+ boolean upgrade = end > start;
+ List result = new ArrayList();
+ int current = start;
+ while (upgrade ? current < end : current > end) {
+ Map targets = mMigrations.get(Integer.valueOf(current));
+ if (targets == null) {
+ return null;
+ }
+ Migration best = null;
+ for (Map.Entry e : targets.entrySet()) {
+ int to = e.getKey().intValue();
+ boolean usable = upgrade ? to <= end && to > current : to >= end && to < current;
+ if (usable && (best == null || (upgrade ? to > best.endVersion : to < best.endVersion))) {
+ best = e.getValue();
+ }
+ }
+ if (best == null) {
+ return null;
+ }
+ result.add(best);
+ current = best.endVersion;
+ }
+ return result;
+ }
+ }
+
+ /// Configures and creates a database.
+ public static class Builder {
+ final Class mDatabaseClass;
+ final String mName;
+ final Context mContext;
+ final List mCallbacks = new ArrayList();
+ final List