diff --git a/Ports/CLDC11/src/java/lang/Class.java b/Ports/CLDC11/src/java/lang/Class.java index d79fe6660e..2e640a47ab 100644 --- a/Ports/CLDC11/src/java/lang/Class.java +++ b/Ports/CLDC11/src/java/lang/Class.java @@ -278,4 +278,8 @@ public Constructor getEnclosingConstructor() { public boolean isLocalClass() { return false; } + + public boolean isRecord() { + return this != java.lang.Record.class && java.lang.Record.class.isAssignableFrom(this); + } } diff --git a/Ports/CLDC11/src/java/lang/Record.java b/Ports/CLDC11/src/java/lang/Record.java new file mode 100644 index 0000000000..d870566c2b --- /dev/null +++ b/Ports/CLDC11/src/java/lang/Record.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 java.lang; + +/** + * Base class for Java record types. + */ +public abstract class Record { + protected Record() { + } + + public abstract boolean equals(Object obj); + + public abstract int hashCode(); + + public abstract String toString(); +} diff --git a/docs/website/content/blog/official-experimental-java-17-support.md b/docs/website/content/blog/official-experimental-java-17-support.md new file mode 100644 index 0000000000..5a252e1aee --- /dev/null +++ b/docs/website/content/blog/official-experimental-java-17-support.md @@ -0,0 +1,153 @@ +--- +title: Official Experimental Java 17 Support for Codename One Projects +date: '2026-03-14' +author: Shai Almog +slug: official-experimental-java-17-support +url: /blog/official-experimental-java-17-support/ +description: Codename One projects generated with the Initializr can now use official experimental Java 17 support, with record support arriving in Codename One 7.0.229 on March 20, 2026, and more JDK support planned next. +feed_html: 'Official Experimental Java 17 Support for Codename One Projects Codename One projects generated with the Initializr can now use official experimental Java 17 support, with record support arriving in Codename One 7.0.229 on March 20, 2026, and more JDK support planned next.' +--- + +![Official Experimental Java 17 Support for Codename One Projects](/blog/official-experimental-java-17-support.jpg) + +We now have official experimental support for Java 17 in Codename One projects. + +This is available through the [Initializr](/initializr/). To use it you should generate a new project and select **Java 17** during project creation. + +Support for Java 17 required work across the toolchain, the generated projects and several targets. This wasn't just a matter of changing a version number and hoping for the best. + +## How This Works + +You can use these new Java 17 projects with practically any JDK for the build itself. We tested this with JDK 21 and even JDK 25. In other words, you select Java 17 for the generated project, but your local machine does not need to run on JDK 17 just to build it. + +## Caveats + +There are a few caveats you should know before jumping in: + +- The desktop target is not currently supported for Java 17 projects. If there is demand we can add that. You can still use the `jar` target to build desktop applications with Codename One, and that works fine with Java 17. +- UWP will not be supported. That target was already deprecated, so there is no point in doing the extra work there. +- Other targets should just work. + +## What Java 17 Syntax Works? + +This support includes modern language syntax that makes day to day code much nicer to write. + +At the moment this includes features like `var`, switch expressions and text blocks. Record support is planned for Codename One `7.0.229`, slated for release on March 20, 2026. + +For example, `var` lets us remove obvious type boilerplate from local variables: + +```java +var greeting = "Hello"; +var target = "Codename One"; +``` + +Switch expressions are more concise and make it easier to return a value directly from the switch: + +```java +var message = switch (greeting.length()) { + case 5 -> greeting + " " + target; + default -> "unexpected"; +}; +``` + +Text blocks are also supported, so multi-line strings are much more readable: + +```java +var textBlock = """ + Java 17 language features + should compile in tests. + """; +``` + +Record syntax is also queued for Codename One `7.0.229`, slated for release on March 20, 2026, so Java 17 projects will be able to use compact immutable data carriers too. + +For example, this record will compile once `7.0.229` is available on March 20, 2026: + +```java +record Person(String name, int age) {} + +var person = new Person("Duke", 29); +System.out.println(person.name()); +``` + +That means you can start using language improvements that make code cleaner and easier to read, and records will join that list in `7.0.229` on March 20, 2026. + +This still does not include support for newer JDK APIs such as streams. As mentioned in the previous post, if you need streams today there is already a cn1lib solution for that, and we hope to improve the built-in story over time. + +## What About Java 21 and 25? + +Since we mentioned JDK 21 and 25 above, it is worth clarifying the roadmap. + +We do plan to add support for newer language levels in a coming update. That is a different challenge, because Android only supports up to Java 17 today. + +We would still like to introduce that support, but at the moment Java 17 is the more important milestone. + +## Other Updates Worth Mentioning + +A few other useful things landed over the past week. + +The [Initializr](/initializr/) now includes more advanced CSS theme editing with live preview, and there were several follow-up fixes to make theming behave more reliably. This is relevant if you are starting a fresh project anyway, because the [Initializr](/initializr/) has become a much stronger starting point than it was even a couple of weeks ago. + +We also finished updating all macOS build servers to use Xcode 26. + +Another useful update is a newer version of the Codename One Bluetooth LE cn1lib, available in the [bluetoothle-codenameone repository](https://github.com/codenameone/bluetoothle-codenameone). This library now uses a native Codename One bridge implementation adapted from the original Bluetooth LE plugin lineage. + +If you are using a Maven-based Codename One project, add it with: + +```xml + + com.codenameone + cn1-bluetooth-lib + 1.0.0 + pom + +``` + +If you are using a classic Codename One project, you can still integrate it as a `.cn1lib` by adding the library to your project's `lib/` directory, running `Refresh Libs`, and rebuilding. The library requires Java 8. + +Here is a small usage sample: + +```java +final Bluetooth bt = new Bluetooth(); +Form main = new Form("Bluetooth Demo"); +main.setLayout(new BoxLayout(BoxLayout.Y_AXIS)); +main.add(new Button(new Command("enable bluetooth") { + @Override + public void actionPerformed(ActionEvent evt) { + try { + if (!bt.isEnabled()) { + bt.enable(); + } + if (!bt.hasPermission()) { + bt.requestPermission(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } +})); +main.add(new Button(new Command("initialize") { + @Override + public void actionPerformed(ActionEvent evt) { + try { + bt.initialize(true, false, "bluetoothleplugin"); + } catch (IOException ex) { + ex.printStackTrace(); + } + } +})); +``` + +## Try Java 17 on a Real Project + +If you have been waiting for a more modern Java syntax level in Codename One, this is the time to give it a try. + +Create a new project with the [Initializr](/initializr/), select Java 17 and see how it feels in a real application. If you run into issues on Android, iOS, JavaScript or any other target, let us know. That feedback is what will help us polish this support and move it forward. + +--- + +## Discussion + +_Join the conversation via GitHub Discussions._ + +{{< giscus >}} diff --git a/docs/website/static/blog/official-experimental-java-17-support.jpg b/docs/website/static/blog/official-experimental-java-17-support.jpg new file mode 100644 index 0000000000..6fac66db1c Binary files /dev/null and b/docs/website/static/blog/official-experimental-java-17-support.jpg differ diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index c4cd8ffaee..4377ebab93 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -3607,9 +3607,15 @@ public void usesClassMethod(String cls, String method) { String compileSdkVersion = "'android-21'"; - String java8P2 = ""; - if(useJava8SourceLevel) { - java8P2 = " compileOptions {\n" + + int projectJavaVersion = parseVersionStringAsInt(request.getArg("java.version", "8")); + String javaCompileOptions = ""; + if (projectJavaVersion >= 17 && useGradle8) { + javaCompileOptions = " compileOptions {\n" + + " sourceCompatibility JavaVersion.toVersion(17)\n" + + " targetCompatibility JavaVersion.toVersion(17)\n" + + " }\n"; + } else if(useJava8SourceLevel) { + javaCompileOptions = " compileOptions {\n" + " sourceCompatibility JavaVersion.VERSION_1_8\n" + " targetCompatibility JavaVersion.VERSION_1_8\n" + " }\n"; @@ -3770,7 +3776,7 @@ public void usesClassMethod(String cls, String method) { + multidex + request.getArg("android.xgradle_default_config", "") + " }\n" - + java8P2 + + javaCompileOptions + " sourceSets {\n" + " main {\n" + " aidl.srcDirs = ['src/main/java']\n" diff --git a/scripts/hellocodenameone/common/codenameone_settings.properties b/scripts/hellocodenameone/common/codenameone_settings.properties index 20eb38b87d..4f49400ea4 100644 --- a/scripts/hellocodenameone/common/codenameone_settings.properties +++ b/scripts/hellocodenameone/common/codenameone_settings.properties @@ -1,12 +1,14 @@ codename1.android.keystore= codename1.android.keystoreAlias= codename1.android.keystorePassword= +codename1.arg.android.useAndroidX=true +codename1.arg.ios.applicationQueriesSchemes=cydia codename1.arg.ios.newStorageLocation=true codename1.arg.ios.uiscene=false codename1.arg.java.version=17 +codename1.cssTheme=true codename1.displayName=HelloCodenameOne codename1.icon=icon.png -codename1.arg.android.useAndroidX=true codename1.ios.appid=Q5GHSKAL2F.com.codenameone.examples.hellocodenameone codename1.ios.certificate= codename1.ios.certificatePassword= @@ -28,4 +30,3 @@ codename1.rim.signtoolDb= codename1.secondaryTitle=Hello World codename1.vendor=CodenameOne codename1.version=1.0 -codename1.cssTheme=true diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index dcdcb95be9..1a755e127b 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -74,6 +74,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { new OrientationLockScreenshotTest(), new InPlaceEditViewTest(), new BytecodeTranslatorRegressionTest(), + new Java17Tests(), new BackgroundThreadUiAccessTest(), new VPNDetectionAPITest(), new CallDetectionAPITest(), diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Java17Tests.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Java17Tests.java new file mode 100644 index 0000000000..9cdbd144fb --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Java17Tests.java @@ -0,0 +1,62 @@ +package com.codenameone.examples.hellocodenameone.tests; + +public class Java17Tests extends BaseTest { + record MyRecord(int val, String otherVal) { + @Override + public String toString() { + return "MyRecord[val=" + val + ", otherVal=" + otherVal + "]"; + } + + @Override + public int hashCode() { + return 31 * val + otherVal.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MyRecord)) { + return false; + } + MyRecord other = (MyRecord)obj; + return val == other.val && otherVal.equals(other.otherVal); + } + } + + @Override + public boolean shouldTakeScreenshot() { + return false; + } + + @Override + public boolean runTest() throws Exception { + try { + var greeting = "Hello"; + var target = "Codename One"; + var record = new MyRecord(2, "V"); + + var message = switch (greeting.length()) { + case 5 -> greeting + " " + target; + default -> "unexpected"; + }; + + var textBlock = """ + Java 17 language features + should compile in tests. + """; + + assertEqual("Hello Codename One", message); + assertEqual("Java 17 language features\nshould compile in tests.\n", textBlock); + assertEqual(2, record.val()); + assertEqual("V", record.otherVal()); + assertEqual("MyRecord[val=2, otherVal=V]", record.toString()); + assertEqual(148, record.hashCode()); + assertTrue(record.equals(new MyRecord(2, "V"))); + return true; + } catch (Throwable t) { + fail(String.valueOf(t)); + return false; + } finally { + done(); + } + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java index c1c1796628..91b37b9595 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java @@ -17,6 +17,7 @@ public boolean runTest() { protected void onShowCompleted() { CN.lockOrientation(false); waitForOrientation(this, false, () -> { + waitFor(50); Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot("landscape"); CN.lockOrientation(true); waitForOrientation(this, true, OrientationLockScreenshotTest.this::done); diff --git a/scripts/hellocodenameone/common/src/test/java/com/codenameone/examples/hellocodenameone/Java17LanguageFeaturesTest.java b/scripts/hellocodenameone/common/src/test/java/com/codenameone/examples/hellocodenameone/Java17LanguageFeaturesTest.java deleted file mode 100644 index 0bc56d0612..0000000000 --- a/scripts/hellocodenameone/common/src/test/java/com/codenameone/examples/hellocodenameone/Java17LanguageFeaturesTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.codenameone.examples.hellocodenameone; - -import com.codename1.testing.AbstractTest; - -public class Java17LanguageFeaturesTest extends AbstractTest { - - @Override - public boolean runTest() throws Exception { - var greeting = "Hello"; - var target = "Codename One"; - - var message = switch (greeting.length()) { - case 5 -> greeting + " " + target; - default -> "unexpected"; - }; - - var textBlock = """ - Java 17 language features - should compile in tests. - """; - - assertEqual("Hello Codename One", message); - assertEqual("Java 17 language features\nshould compile in tests.\n", textBlock); - - return true; - } -} diff --git a/vm/JavaAPI/src/java/lang/Class.java b/vm/JavaAPI/src/java/lang/Class.java index 8422cc3d90..debb2c8d0e 100644 --- a/vm/JavaAPI/src/java/lang/Class.java +++ b/vm/JavaAPI/src/java/lang/Class.java @@ -341,4 +341,8 @@ public Constructor getEnclosingConstructor() { public boolean isLocalClass() { return false; } + + public boolean isRecord() { + return this != java.lang.Record.class && java.lang.Record.class.isAssignableFrom(this); + } } diff --git a/vm/JavaAPI/src/java/lang/Record.java b/vm/JavaAPI/src/java/lang/Record.java new file mode 100644 index 0000000000..d870566c2b --- /dev/null +++ b/vm/JavaAPI/src/java/lang/Record.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 java.lang; + +/** + * Base class for Java record types. + */ +public abstract class Record { + protected Record() { + } + + public abstract boolean equals(Object obj); + + public abstract int hashCode(); + + public abstract String toString(); +}