diff --git a/.github/workflows/car-android.yml b/.github/workflows/car-android.yml index 4db26465fa2..3cbf7b9c431 100644 --- a/.github/workflows/car-android.yml +++ b/.github/workflows/car-android.yml @@ -42,6 +42,12 @@ jobs: - uses: actions/checkout@v6 - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV + - name: Free Disk Space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android/sdk/ndk + sudo rm -rf /opt/ghc + sudo rm -rf /usr/share/swift - name: Cache codenameone-tools uses: actions/cache@v5 with: diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index e6274fd53e3..9f51c1b3d38 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -121,7 +121,6 @@ jobs: - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - name: Free Disk Space - if: matrix.id != 'default' run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/lib/android/sdk/ndk diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidCalendarSource.java b/Ports/Android/src/com/codename1/impl/android/AndroidCalendarSource.java index 7d79b8b160b..55cd880452d 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidCalendarSource.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidCalendarSource.java @@ -54,6 +54,8 @@ /** Android Calendar Provider integration. */ final class AndroidCalendarSource extends LocalCalendarSource { + // Events and Instances use the same Calendar Provider selection column. + private static final String CALENDAR_ID_SELECTION_COLUMN = Events.CALENDAR_ID; private static final String[] EVENT_COLUMNS = {Events._ID, Events.CALENDAR_ID, Events.TITLE, Events.DESCRIPTION, Events.EVENT_LOCATION, Events.DTSTART, Events.DTEND, Events.DURATION, Events.EVENT_TIMEZONE, Events.ALL_DAY, Events.RRULE, Events.STATUS, Events.AVAILABILITY, @@ -119,9 +121,8 @@ private CalendarPage queryEventsBlocking(CalendarQuery query) List args = new ArrayList(); boolean expandInstances = query != null && (query.getStartTime() != null || query.getEndTime() != null); - String calendarColumn = expandInstances ? Instances.CALENDAR_ID : Events.CALENDAR_ID; if (query != null && query.getCalendarId() != null) { - where.append(" AND ").append(calendarColumn).append("=?"); + where.append(" AND ").append(CALENDAR_ID_SELECTION_COLUMN).append("=?"); args.add(query.getCalendarId()); } where.append(" AND ").append(Events.DELETED).append("=0"); diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index 4aaa6196968..187c7bb2fc0 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -786,8 +786,8 @@ This is great as it allows apps to be installed with a single click and no permi ===== Enabling permissions -// vale-skip: write-good.TooWordy — 'minimum of API 33' is the precise term for the SDK floor. -Codename One's Gradle 8 based Android builder detects the highest Android SDK you've installed and uses that value (with a minimum of API 33) for both the compile and target SDK versions, so the modern runtime permission flow is enabled by default. If you override the target version through the `android.targetSDKVersion` build hint the builder will honour it, but lowering the target may disable some compatibility libraries. Keeping the target current is strongly recommended for Play Store compliance. +// vale-skip: write-good.TooWordy — 'minimum of API 36' is the precise term for the SDK floor. +By default, Codename One's Gradle 8 based Android builder uses the highest Android SDK you've installed, with a minimum of API 36, for both the compile and target SDK versions. If you override the target through the `android.targetSDKVersion` build hint, the builder honors it while keeping the compile SDK at least as high as the target SDK. Lowering the target may disable some compatibility libraries. Keeping the target current is strongly recommended for Play Store compliance. ===== Permission prompts @@ -803,7 +803,7 @@ If you explicitly lower the target SDK (for example: `android.targetSDKVersion=2 .Install UI when using the old permissions system image::img/marshmallow-permissions-level21.png[Install UI when using the old permissions system,scaledwidth=20%] -When you keep the default target (API 33+) the installer defers to the runtime permission flow and the installation UI looks like this instead: +When you keep the default target (API 36+) the installer defers to the runtime permission flow and the installation UI looks like this instead: .Install UI when using the new permissions system image::img/marshmallow-permissions-level23.png[Install UI when using the new permissions system,scaledwidth=20%] diff --git a/maven/android/pom.xml b/maven/android/pom.xml index 2afe8497b10..7cc093111e0 100644 --- a/maven/android/pom.xml +++ b/maven/android/pom.xml @@ -43,7 +43,7 @@ Max Low - false + true true ${project.build.directory} ${project.basedir}/../../Ports/Android/spotbugs-exclude.xml @@ -53,7 +53,7 @@ spotbugs verify - spotbugs + check 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 5a198a861a8..93565eb7322 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 @@ -759,10 +759,12 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc } if (useGradle8) { + // Build Tools and platform SDK versions are independent; compileSdk is + // raised to at least targetSdk when the Gradle project is generated below. maxBuildToolsVersionInt = Math.max(33, maxBuildToolsVersionInt); maxBuildToolsVersion = "" + maxBuildToolsVersionInt; - maxPlatformVersionInt = Math.max(33, maxPlatformVersionInt); + maxPlatformVersionInt = Math.max(36, maxPlatformVersionInt); maxPlatformVersion = "" + maxPlatformVersionInt; } @@ -4995,6 +4997,7 @@ public void usesClassMethod(String cls, String method) { compileSdkVersion = "36"; supportLibVersion = "28"; } + compileSdkVersion = ensureCompileSdkAtLeastTarget(compileSdkVersion, targetNumber); jcenter = " google()\n" + " jcenter()\n" + @@ -6200,7 +6203,23 @@ private void initPlayServiceVersions(BuildRequest request) { } } - private Integer parseSdkInt(String value) { + // Package-private for direct unit testing; this is not part of the builder API. + static String ensureCompileSdkAtLeastTarget(String compileSdkVersion, String targetSdkVersion) { + Integer compileSdkInt = parseSdkInt(compileSdkVersion); + Integer targetSdkInt = parseSdkInt(targetSdkVersion); + if (targetSdkInt == null) { + return compileSdkVersion; + } + if (compileSdkVersion == null || compileSdkVersion.trim().isEmpty()) { + return String.valueOf(targetSdkInt); + } + if (compileSdkInt != null && targetSdkInt > compileSdkInt) { + return String.valueOf(targetSdkInt); + } + return compileSdkVersion; + } + + private static Integer parseSdkInt(String value) { if (value == null) { return null; } diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AndroidGradleBuilderSdkVersionTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AndroidGradleBuilderSdkVersionTest.java new file mode 100644 index 00000000000..0fe66904647 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AndroidGradleBuilderSdkVersionTest.java @@ -0,0 +1,60 @@ +/* + * 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 com.codename1.builders; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AndroidGradleBuilderSdkVersionTest { + + @Test + void raisesCompileSdkToTargetSdk() { + assertEquals("36", + AndroidGradleBuilder.ensureCompileSdkAtLeastTarget("33", "36")); + } + + @Test + void doesNotLowerCompileSdk() { + assertEquals("36", + AndroidGradleBuilder.ensureCompileSdkAtLeastTarget("36", "35")); + } + + @Test + void handlesLegacyCompileSdkNotation() { + assertEquals("36", + AndroidGradleBuilder.ensureCompileSdkAtLeastTarget("'android-21'", "36")); + } + + @Test + void preservesNonNumericCompileSdk() { + assertEquals("'android-Baklava'", + AndroidGradleBuilder.ensureCompileSdkAtLeastTarget("'android-Baklava'", "36")); + } + + @Test + void suppliesMissingCompileSdk() { + assertEquals("36", + AndroidGradleBuilder.ensureCompileSdkAtLeastTarget(null, "36")); + } +}