From 8fe6985b34538e51f79b075db34ac085e27b6edb Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 05:18:53 +0300
Subject: [PATCH 01/14] Fix AdMob iOS linking, Android banner sizing, and mock
interstitial display
---
.../workflows/ad-cn1lib-ios-native-check.yml | 17 ++++
.../workflows/admob-android-runtime-check.yml | 45 +++++++++++
.../codename1/ads/admob/AdMobNativeImpl.java | 9 ++-
.../codenameone_library_appended.properties | 4 +-
maven/cn1-admob/common/pom.xml | 2 +
.../codename1/ads/mock/MockAdProvider.java | 51 +++++++++++-
maven/core-unittests/pom.xml | 6 ++
.../com/codename1/ads/MockAdProviderTest.java | 81 +++++++++++++++++++
scripts/check-admob-ios-link.sh | 62 ++++++++++++++
.../cn1lib-api-check/admob-runtime/.gitignore | 3 +
.../cn1lib-api-check/admob-runtime/README.md | 12 +++
.../admob-runtime/build.gradle | 33 ++++++++
.../admob-runtime/gradle.properties | 1 +
.../admob-runtime/settings.gradle | 3 +
.../ads/admob/BannerMeasurementTest.java | 28 +++++++
.../src/main/AndroidManifest.xml | 7 ++
.../codename1/ads/admob/AdMobCallback.java | 15 ++++
.../codename1/ads/admob/ProbeActivity.java | 8 ++
.../impl/android/AndroidImplementation.java | 11 +++
.../impl/android/AndroidNativeUtil.java | 6 ++
.../template.xcodeproj/project.pbxproj | 8 ++
21 files changed, 406 insertions(+), 6 deletions(-)
create mode 100644 .github/workflows/admob-android-runtime-check.yml
create mode 100644 maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
create mode 100755 scripts/check-admob-ios-link.sh
create mode 100644 scripts/cn1lib-api-check/admob-runtime/.gitignore
create mode 100644 scripts/cn1lib-api-check/admob-runtime/README.md
create mode 100644 scripts/cn1lib-api-check/admob-runtime/build.gradle
create mode 100644 scripts/cn1lib-api-check/admob-runtime/gradle.properties
create mode 100644 scripts/cn1lib-api-check/admob-runtime/settings.gradle
create mode 100644 scripts/cn1lib-api-check/admob-runtime/src/androidTest/java/com/codename1/ads/admob/BannerMeasurementTest.java
create mode 100644 scripts/cn1lib-api-check/admob-runtime/src/main/AndroidManifest.xml
create mode 100644 scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/AdMobCallback.java
create mode 100644 scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/ProbeActivity.java
create mode 100644 scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidImplementation.java
create mode 100644 scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidNativeUtil.java
diff --git a/.github/workflows/ad-cn1lib-ios-native-check.yml b/.github/workflows/ad-cn1lib-ios-native-check.yml
index 738daa72248..0d61b1b62ce 100644
--- a/.github/workflows/ad-cn1lib-ios-native-check.yml
+++ b/.github/workflows/ad-cn1lib-ios-native-check.yml
@@ -20,6 +20,8 @@ on:
- 'maven/cn1-admob/**'
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
+ - 'scripts/check-admob-ios-link.sh'
+ - 'vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
@@ -29,6 +31,8 @@ on:
- 'maven/cn1-admob/**'
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
+ - 'scripts/check-admob-ios-link.sh'
+ - 'vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj'
- 'vm/ByteCodeTranslator/src/cn1_globals.h'
- 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
@@ -200,3 +204,16 @@ jobs:
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO \
build
+
+ link-admob-app:
+ name: Link AdMob app (${{ matrix.sdk }})
+ runs-on: macos-15
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [iphoneos, iphonesimulator]
+ steps:
+ - uses: actions/checkout@v6
+ - run: brew install xcodegen
+ - name: Link the native bridge and SDK into an app
+ run: scripts/check-admob-ios-link.sh "$RUNNER_TEMP/admob-link" "${{ matrix.sdk }}"
diff --git a/.github/workflows/admob-android-runtime-check.yml b/.github/workflows/admob-android-runtime-check.yml
new file mode 100644
index 00000000000..e5f7533bab3
--- /dev/null
+++ b/.github/workflows/admob-android-runtime-check.yml
@@ -0,0 +1,45 @@
+name: AdMob Android banner runtime check
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: [master]
+ paths:
+ - 'maven/cn1-admob/**'
+ - 'scripts/cn1lib-api-check/admob-runtime/**'
+ - '.github/workflows/admob-android-runtime-check.yml'
+ push:
+ branches: [master]
+ paths:
+ - 'maven/cn1-admob/**'
+ - 'scripts/cn1lib-api-check/admob-runtime/**'
+ - '.github/workflows/admob-android-runtime-check.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ banner-measurement:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+ - uses: gradle/actions/setup-gradle@v4
+ with:
+ gradle-version: '8.13'
+ - name: Enable KVM
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+ - uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: 35
+ arch: x86_64
+ disable-animations: true
+ script: gradle -p scripts/cn1lib-api-check/admob-runtime connectedDebugAndroidTest
diff --git a/maven/cn1-admob/android/src/main/java/com/codename1/ads/admob/AdMobNativeImpl.java b/maven/cn1-admob/android/src/main/java/com/codename1/ads/admob/AdMobNativeImpl.java
index 693c02811ca..a9a5e9c7d82 100644
--- a/maven/cn1-admob/android/src/main/java/com/codename1/ads/admob/AdMobNativeImpl.java
+++ b/maven/cn1-admob/android/src/main/java/com/codename1/ads/admob/AdMobNativeImpl.java
@@ -340,7 +340,14 @@ public View createBanner(final int handle, final String adUnitId, final int size
public void run() {
AdView adView = new AdView(activity);
adView.setAdUnitId(adUnitId);
- adView.setAdSize(mapSize(activity, sizeType, widthDp));
+ AdSize size = mapSize(activity, sizeType, widthDp);
+ adView.setAdSize(size);
+ // AndroidPeer reads measured dimensions in its default peer mode.
+ // The AdView has no parent yet, so Android has not measured it:
+ // wrapping it now would cache a 1px preferred height in CN1.
+ adView.measure(
+ View.MeasureSpec.makeMeasureSpec(size.getWidthInPixels(activity), View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(size.getHeightInPixels(activity), View.MeasureSpec.EXACTLY));
banners.put(handle, adView);
out[0] = adView;
}
diff --git a/maven/cn1-admob/common/codenameone_library_appended.properties b/maven/cn1-admob/common/codenameone_library_appended.properties
index f47bb32ba04..d92d1bde09d 100644
--- a/maven/cn1-admob/common/codenameone_library_appended.properties
+++ b/maven/cn1-admob/common/codenameone_library_appended.properties
@@ -1 +1,3 @@
-# Reserved for build hints appended to the consuming app's properties.
+# Google Mobile Ads contains C++ objects even when the host app is Objective-C.
+# Append, preserving any frameworks the application already links.
+codename1.arg.ios.add_libs=;libc++.tbd
diff --git a/maven/cn1-admob/common/pom.xml b/maven/cn1-admob/common/pom.xml
index 6764e82abcd..e02149edf41 100644
--- a/maven/cn1-admob/common/pom.xml
+++ b/maven/cn1-admob/common/pom.xml
@@ -73,6 +73,8 @@
+
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index 2b9277bbebb..8627413f4b8 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -38,10 +38,14 @@
import com.codename1.ads.spi.BannerAdSession;
import com.codename1.ads.spi.FullScreenAdSession;
import com.codename1.ads.spi.NativeAdProvider;
+import com.codename1.ui.Button;
import com.codename1.ui.CN;
+import com.codename1.ui.Command;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
+import com.codename1.ui.Form;
import com.codename1.ui.Label;
+import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.layouts.BorderLayout;
@@ -116,6 +120,8 @@ private static final class MockFullScreen implements FullScreenAdSession {
private final AdFormat format;
private AdSessionCallback cb;
private boolean loaded;
+ private Form adForm;
+ private Form previousForm;
MockFullScreen(AdFormat format) {
this.format = format;
@@ -147,13 +153,48 @@ public void show() {
cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "No ad loaded"));
return;
}
+ if (adForm != null) {
+ cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "An ad is already showing"));
+ return;
+ }
loaded = false;
+ previousForm = CN.getCurrentForm();
+ adForm = new Form("Mock advertisement", new BorderLayout());
+ adForm.add(BorderLayout.CENTER, new Label("Advertisement"));
+ Button close = new Button("Close ad");
+ close.addActionListener(evt -> closeAd(true));
+ adForm.add(BorderLayout.SOUTH, close);
+ adForm.setBackCommand(new Command("Close ad") {
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ closeAd(true);
+ }
+ });
+ adForm.show();
cb.onShown();
- cb.onImpression();
- if (format == AdFormat.REWARDED || format == AdFormat.REWARDED_INTERSTITIAL) {
- cb.onUserEarnedReward(new RewardItem("coins", 10));
+ // A listener may dispose the ad synchronously from onShown().
+ if (adForm != null) {
+ cb.onImpression();
+ }
+ }
+
+ private void closeAd(boolean notify) {
+ if (adForm == null) {
+ return;
+ }
+ Form closing = adForm;
+ Form previous = previousForm;
+ adForm = null;
+ previousForm = null;
+ if (previous != null && CN.getCurrentForm() == closing) {
+ previous.showBack();
+ }
+ if (notify) {
+ if (format == AdFormat.REWARDED || format == AdFormat.REWARDED_INTERSTITIAL) {
+ cb.onUserEarnedReward(new RewardItem("coins", 10));
+ }
+ cb.onDismissed();
}
- cb.onDismissed();
}
@Override
@@ -162,6 +203,8 @@ public void setAutoShowOnForeground(boolean enabled) {
@Override
public void dispose() {
+ loaded = false;
+ closeAd(false);
}
}
diff --git a/maven/core-unittests/pom.xml b/maven/core-unittests/pom.xml
index 584ed4d1e2f..f4a9bfc2347 100644
--- a/maven/core-unittests/pom.xml
+++ b/maven/core-unittests/pom.xml
@@ -167,6 +167,12 @@
+
+ com.codenameone
+ cn1-ads-mock
+ ${project.version}
+ test
+
com.codenameone
codenameone-factory
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
new file mode 100644
index 00000000000..b9894e6a3c4
--- /dev/null
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -0,0 +1,81 @@
+package com.codename1.ads;
+
+import com.codename1.ads.mock.MockAdProvider;
+import com.codename1.junit.FormTest;
+import com.codename1.junit.UITestBase;
+import com.codename1.ui.Button;
+import com.codename1.ui.CN;
+import com.codename1.ui.Form;
+import com.codename1.ui.events.ActionEvent;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.*;
+
+class MockAdProviderTest extends UITestBase {
+ private final List events = new ArrayList();
+
+ @org.junit.jupiter.api.BeforeEach
+ void resetEvents() { events.clear(); }
+
+ private AdListener listener() {
+ return new AdListener() {
+ @Override public void onShown() { events.add("shown"); }
+ @Override public void onImpression() { events.add("impression"); }
+ @Override public void onDismissed() { events.add("dismissed"); }
+ };
+ }
+
+ @FormTest
+ void interstitialStaysVisibleUntilClosedAndRestoresPreviousForm() {
+ Form previous = CN.getCurrentForm();
+ MockAdProvider.install();
+ InterstitialAd ad = new InterstitialAd("mock");
+ ad.setAdListener(listener());
+ ad.load();
+ ad.show();
+ Form showing = CN.getCurrentForm();
+ assertNotSame(previous, showing);
+ assertEquals("Mock advertisement", showing.getTitle());
+ assertEquals(Arrays.asList("shown", "impression"), events);
+ Button close = (Button) showing.getContentPane().getComponentAt(1);
+ close.pressed();
+ close.released();
+ assertSame(previous, CN.getCurrentForm());
+ assertEquals(Arrays.asList("shown", "impression", "dismissed"), events);
+ showing.getBackCommand().actionPerformed(new ActionEvent(showing));
+ assertEquals(3, events.size(), "Closing twice must not repeat callbacks");
+ ad.dispose();
+ }
+
+ @FormTest
+ void rewardedBackCloseDeliversRewardBeforeDismissal() {
+ Form previous = CN.getCurrentForm();
+ MockAdProvider.install();
+ RewardedAd ad = new RewardedAd("mock");
+ ad.setAdListener(listener());
+ ad.setOnUserEarnedRewardListener(reward -> events.add("reward"));
+ ad.load();
+ ad.show();
+ Form showing = CN.getCurrentForm();
+ assertEquals(Arrays.asList("shown", "impression"), events);
+ showing.getBackCommand().actionPerformed(new ActionEvent(showing));
+ assertSame(previous, CN.getCurrentForm());
+ assertEquals(Arrays.asList("shown", "impression", "reward", "dismissed"), events);
+ ad.dispose();
+ }
+
+ @FormTest
+ void disposingVisibleAdRestoresFormWithoutRewardOrDismissal() {
+ Form previous = CN.getCurrentForm();
+ MockAdProvider.install();
+ RewardedAd ad = new RewardedAd("mock");
+ ad.setAdListener(listener());
+ ad.setOnUserEarnedRewardListener(reward -> events.add("reward"));
+ ad.load();
+ ad.show();
+ ad.dispose();
+ assertSame(previous, CN.getCurrentForm());
+ assertEquals(Arrays.asList("shown", "impression"), events);
+ }
+}
diff --git a/scripts/check-admob-ios-link.sh b/scripts/check-admob-ios-link.sh
new file mode 100755
index 00000000000..683aafe02be
--- /dev/null
+++ b/scripts/check-admob-ios-link.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+# Link a real Objective-C app against the shipped bridge and pinned SDK.
+# Usage: scripts/check-admob-ios-link.sh [output-directory] [iphoneos|iphonesimulator]
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+PROBE="${1:-$(mktemp -d /tmp/cn1-admob-link.XXXXXX)}"
+SDK="${2:-iphoneos}"
+mkdir -p "$PROBE/Sources"
+cp "$ROOT"/maven/cn1-admob/ios/src/main/objectivec/* "$PROBE/Sources/"
+cp "$ROOT"/vm/ByteCodeTranslator/src/{cn1_globals.h,cn1_virtual_thread.h} "$PROBE/"
+printf '#pragma once\n' > "$PROBE/cn1_class_method_index.h"
+cat > "$PROBE/Prefix.pch" <<'PCH'
+#import
+#import
+#include "cn1_globals.h"
+PCH
+cat > "$PROBE/Sources/main.m" <<'OBJC'
+#import "com_codename1_ads_admob_AdMobNativeImpl.h"
+// Only generated Java runtime entry points are stubbed. All SDK references
+// must resolve through the real CocoaPods integration and final app linker.
+struct ThreadLocalData* getThreadLocalData(void) { return NULL; }
+JAVA_OBJECT fromNSString(CODENAME_ONE_THREAD_STATE, NSString* str) { return JAVA_NULL; }
+void com_codename1_ads_admob_AdMobCallback_fire___int_int_int_java_lang_String_java_lang_String_int(
+ CN1_THREAD_STATE_MULTI_ARG JAVA_INT handle, JAVA_INT event, JAVA_INT code,
+ JAVA_OBJECT message, JAVA_OBJECT rewardType, JAVA_INT rewardAmount) {}
+int main(int argc, char** argv) {
+ @autoreleasepool {
+ return [[[com_codename1_ads_admob_AdMobNativeImpl alloc] init] isSupported] ? 0 : 1;
+ }
+}
+OBJC
+python3 - "$ROOT" "$PROBE" <<'PY'
+import json, pathlib, re, sys
+root, probe = map(pathlib.Path, sys.argv[1:])
+props = root / 'maven/cn1-admob/common'
+pod = re.search(r'^codename1.arg.ios.pods=(.+)$', (props / 'codenameone_library_required.properties').read_text(), re.M).group(1)
+name, version = pod.split(' ', 1)
+(probe / 'Podfile').write_text("platform :ios, '14.0'\ntarget 'AdMobLinkProbe' do\n use_frameworks!\n pod '%s', '%s'\nend\n" % (name, version))
+# Consume the same search paths and library hints as a generated CN1 app.
+template = (root / 'vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj').read_text()
+blocks = re.findall(r'LIBRARY_SEARCH_PATHS = \((.*?)\);', template, re.S)
+paths = re.findall(r'"([^"]+)"', blocks[0])
+assert all(re.findall(r'"([^"]+)"', block) == paths for block in blocks)
+paths = [p for p in paths if 'template-src' not in p]
+appended = (props / 'codenameone_library_appended.properties').read_text()
+match = re.search(r'^codename1.arg.ios.add_libs=(.*)$', appended, re.M)
+libs = [] if not match else [v for v in match.group(1).split(';') if v]
+settings = {'CLANG_ENABLE_MODULES': 'YES', 'CLANG_ENABLE_OBJC_ARC': 'NO',
+ 'CODE_SIGNING_ALLOWED': 'NO', 'GENERATE_INFOPLIST_FILE': 'YES',
+ 'GCC_PREFIX_HEADER': 'Prefix.pch', 'GCC_PRECOMPILE_PREFIX_HEADER': 'NO',
+ 'HEADER_SEARCH_PATHS': ['$(inherited)', '$(SRCROOT)'],
+ 'LIBRARY_SEARCH_PATHS': paths, 'OTHER_LDFLAGS': ['$(inherited)', '-ObjC'],
+ 'DEAD_CODE_STRIPPING': 'NO'}
+project = {'name': 'AdMobLinkProbe', 'options': {'bundleIdPrefix': 'com.codenameone.test', 'deploymentTarget': {'iOS': '14.0'}},
+ 'targets': {'AdMobLinkProbe': {'type': 'application', 'platform': 'iOS', 'sources': ['Sources'],
+ 'dependencies': [{'sdk': lib} for lib in libs], 'settings': {'base': settings}}}}
+(probe / 'project.yml').write_text(json.dumps(project, indent=2))
+PY
+(cd "$PROBE" && xcodegen generate && pod install)
+xcodebuild -workspace "$PROBE/AdMobLinkProbe.xcworkspace" -scheme AdMobLinkProbe \
+ -configuration Release -sdk "$SDK" -derivedDataPath "$PROBE/build-$SDK" \
+ CODE_SIGNING_ALLOWED=NO build
diff --git a/scripts/cn1lib-api-check/admob-runtime/.gitignore b/scripts/cn1lib-api-check/admob-runtime/.gitignore
new file mode 100644
index 00000000000..aa8487e5205
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/.gitignore
@@ -0,0 +1,3 @@
+.gradle/
+build/
+local.properties
diff --git a/scripts/cn1lib-api-check/admob-runtime/README.md b/scripts/cn1lib-api-check/admob-runtime/README.md
new file mode 100644
index 00000000000..b0d6f396485
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/README.md
@@ -0,0 +1,12 @@
+# AdMob banner measurement regression
+
+With JDK 17, Android SDK 36, Gradle 8.13, and a running Android emulator:
+
+```sh
+gradle -p scripts/cn1lib-api-check/admob-runtime connectedDebugAndroidTest
+```
+
+Compiles the shipped native implementation against its declared Google SDKs.
+Checks all five banner formats have their SDK pixel dimensions before CN1 wraps
+or attaches the view, without requesting ads. Only the CN1 activity/thread bridge
+and Java callback sink are replaced; Android and AdView are real.
diff --git a/scripts/cn1lib-api-check/admob-runtime/build.gradle b/scripts/cn1lib-api-check/admob-runtime/build.gradle
new file mode 100644
index 00000000000..a12e0d88b18
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/build.gradle
@@ -0,0 +1,33 @@
+plugins { id 'com.android.application' version '8.13.2' }
+
+def root = file('../../..')
+def props = new Properties()
+file("$root/maven/cn1-admob/common/codenameone_library_required.properties").withInputStream { props.load(it) }
+
+android {
+ namespace 'com.codename1.ads.admob.probe'
+ compileSdk 36
+ defaultConfig {
+ applicationId 'com.codename1.ads.admob.probe'
+ minSdk 23
+ targetSdk 36
+ testInstrumentationRunner 'android.test.InstrumentationTestRunner'
+ testInstrumentationRunnerArguments class: 'com.codename1.ads.admob.BannerMeasurementTest'
+ }
+ useLibrary 'android.test.runner'
+ useLibrary 'android.test.base'
+ sourceSets.main.java.srcDirs += ["$root/maven/cn1-admob/android/src/main/java"]
+ compileOptions { sourceCompatibility JavaVersion.VERSION_1_8; targetCompatibility JavaVersion.VERSION_1_8 }
+}
+dependencies {
+ // Compile and run against precisely the dependencies shipped to apps.
+ (props.getProperty('codename1.arg.android.gradleDep') =~ /'([^']+)'/).each { implementation it[1] }
+}
+
+tasks.register('stageErrorCodes', Copy) {
+ from "$root/maven/cn1-admob/common/src/main/java"
+ include '**/AdMobErrorCodes.java'
+ into layout.buildDirectory.dir('generated/common')
+}
+android.sourceSets.main.java.srcDir layout.buildDirectory.dir('generated/common')
+tasks.named('preBuild').configure { dependsOn 'stageErrorCodes' }
diff --git a/scripts/cn1lib-api-check/admob-runtime/gradle.properties b/scripts/cn1lib-api-check/admob-runtime/gradle.properties
new file mode 100644
index 00000000000..5bac8ac5046
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/gradle.properties
@@ -0,0 +1 @@
+android.useAndroidX=true
diff --git a/scripts/cn1lib-api-check/admob-runtime/settings.gradle b/scripts/cn1lib-api-check/admob-runtime/settings.gradle
new file mode 100644
index 00000000000..509a8e9018a
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/settings.gradle
@@ -0,0 +1,3 @@
+pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } }
+dependencyResolutionManagement { repositories { google(); mavenCentral() } }
+rootProject.name = 'admob-runtime'
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/androidTest/java/com/codename1/ads/admob/BannerMeasurementTest.java b/scripts/cn1lib-api-check/admob-runtime/src/androidTest/java/com/codename1/ads/admob/BannerMeasurementTest.java
new file mode 100644
index 00000000000..4d7427358f6
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/src/androidTest/java/com/codename1/ads/admob/BannerMeasurementTest.java
@@ -0,0 +1,28 @@
+package com.codename1.ads.admob;
+
+import android.test.ActivityInstrumentationTestCase2;
+import com.google.android.gms.ads.AdView;
+import com.google.android.gms.ads.AdSize;
+
+public class BannerMeasurementTest extends ActivityInstrumentationTestCase2 {
+ public BannerMeasurementTest() { super(ProbeActivity.class); }
+
+ public void testBannerHasSdkDimensionsBeforePeerOrAdLoad() throws Throwable {
+ final ProbeActivity activity = getActivity();
+ runTestOnUiThread(new Runnable() {
+ public void run() {
+ AdMobNativeImpl bridge = new AdMobNativeImpl();
+ for (int format = 0; format <= 4; format++) {
+ AdView banner = (AdView) bridge.createBanner(format,
+ "ca-app-pub-3940256099942544/6300978111", format, 320);
+ assertNull("Must be sized before it is attached to CN1", banner.getParent());
+ AdSize size = banner.getAdSize();
+ assertEquals(size.getWidthInPixels(activity), banner.getMeasuredWidth());
+ assertEquals(size.getHeightInPixels(activity), banner.getMeasuredHeight());
+ assertTrue("A SOUTH layout must reserve visible ad height", banner.getMeasuredHeight() > 1);
+ bridge.disposeBanner(format);
+ }
+ }
+ });
+ }
+}
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/AndroidManifest.xml b/scripts/cn1lib-api-check/admob-runtime/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..dd29704a13e
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/AdMobCallback.java b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/AdMobCallback.java
new file mode 100644
index 00000000000..15d516f67c4
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/AdMobCallback.java
@@ -0,0 +1,15 @@
+package com.codename1.ads.admob;
+
+// No Java runtime is needed to measure a native banner. No ad requests are sent.
+public final class AdMobCallback {
+ public static final int LOADED = 1;
+ public static final int FAILED = 2;
+ public static final int SHOWN = 3;
+ public static final int SHOW_FAILED = 4;
+ public static final int DISMISSED = 5;
+ public static final int IMPRESSION = 6;
+ public static final int CLICKED = 7;
+ public static final int REWARD = 8;
+ public static final int CONSENT_COMPLETE = 9;
+ public static void fire(int handle, int event, int code, String message, String rewardType, int amount) {}
+}
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/ProbeActivity.java b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/ProbeActivity.java
new file mode 100644
index 00000000000..ebb3027794b
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/ProbeActivity.java
@@ -0,0 +1,8 @@
+package com.codename1.ads.admob;
+
+public class ProbeActivity extends android.app.Activity {
+ @Override public void onCreate(android.os.Bundle state) {
+ super.onCreate(state);
+ com.codename1.impl.android.AndroidNativeUtil.activity = this;
+ }
+}
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidImplementation.java b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidImplementation.java
new file mode 100644
index 00000000000..afd16dbaaac
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidImplementation.java
@@ -0,0 +1,11 @@
+package com.codename1.impl.android;
+
+public final class AndroidImplementation {
+ public static void runOnUiThreadAndBlock(Runnable task) {
+ // Instrumentation invokes createBanner on Android's UI thread.
+ if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) {
+ throw new AssertionError("The probe must run on the UI thread");
+ }
+ task.run();
+ }
+}
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidNativeUtil.java b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidNativeUtil.java
new file mode 100644
index 00000000000..5c97e266078
--- /dev/null
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidNativeUtil.java
@@ -0,0 +1,6 @@
+package com.codename1.impl.android;
+
+public final class AndroidNativeUtil {
+ public static android.app.Activity activity;
+ public static android.app.Activity getActivity() { return activity; }
+}
diff --git a/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj b/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj
index 304a953222f..37c451c9b06 100644
--- a/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj
+++ b/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj
@@ -309,6 +309,10 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/template-src",
+ // Prebuilt pods can require Swift compatibility archives even in
+ // an Objective-C app. TOOLCHAIN_DIR can point at the Metal toolchain.
+ "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)",
+ "$(SDKROOT)/usr/lib/swift",
);
};
name = Debug;
@@ -352,6 +356,10 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/template-src",
+ // Prebuilt pods can require Swift compatibility archives even in
+ // an Objective-C app. TOOLCHAIN_DIR can point at the Metal toolchain.
+ "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)",
+ "$(SDKROOT)/usr/lib/swift",
);
};
name = Release;
From 1c559afb16bc10086d77fb50b3fae16e752ec826 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 05:20:30 +0300
Subject: [PATCH 02/14] Add required copyright headers to advertising
regression tests
---
.../com/codename1/ads/MockAdProviderTest.java | 22 +++++++++++++++++++
.../ads/admob/BannerMeasurementTest.java | 22 +++++++++++++++++++
.../codename1/ads/admob/AdMobCallback.java | 22 +++++++++++++++++++
.../codename1/ads/admob/ProbeActivity.java | 22 +++++++++++++++++++
.../impl/android/AndroidImplementation.java | 22 +++++++++++++++++++
.../impl/android/AndroidNativeUtil.java | 22 +++++++++++++++++++
6 files changed, 132 insertions(+)
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index b9894e6a3c4..a3b6e892c16 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -1,3 +1,25 @@
+/*
+ * 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.ads;
import com.codename1.ads.mock.MockAdProvider;
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/androidTest/java/com/codename1/ads/admob/BannerMeasurementTest.java b/scripts/cn1lib-api-check/admob-runtime/src/androidTest/java/com/codename1/ads/admob/BannerMeasurementTest.java
index 4d7427358f6..76fe3db739a 100644
--- a/scripts/cn1lib-api-check/admob-runtime/src/androidTest/java/com/codename1/ads/admob/BannerMeasurementTest.java
+++ b/scripts/cn1lib-api-check/admob-runtime/src/androidTest/java/com/codename1/ads/admob/BannerMeasurementTest.java
@@ -1,3 +1,25 @@
+/*
+ * 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.ads.admob;
import android.test.ActivityInstrumentationTestCase2;
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/AdMobCallback.java b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/AdMobCallback.java
index 15d516f67c4..106e7962fe8 100644
--- a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/AdMobCallback.java
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/AdMobCallback.java
@@ -1,3 +1,25 @@
+/*
+ * 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.ads.admob;
// No Java runtime is needed to measure a native banner. No ad requests are sent.
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/ProbeActivity.java b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/ProbeActivity.java
index ebb3027794b..7ece17a0155 100644
--- a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/ProbeActivity.java
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/ads/admob/ProbeActivity.java
@@ -1,3 +1,25 @@
+/*
+ * 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.ads.admob;
public class ProbeActivity extends android.app.Activity {
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidImplementation.java b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidImplementation.java
index afd16dbaaac..15d4eeed2b1 100644
--- a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidImplementation.java
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidImplementation.java
@@ -1,3 +1,25 @@
+/*
+ * 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.impl.android;
public final class AndroidImplementation {
diff --git a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidNativeUtil.java b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidNativeUtil.java
index 5c97e266078..1b13db08411 100644
--- a/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidNativeUtil.java
+++ b/scripts/cn1lib-api-check/admob-runtime/src/main/java/com/codename1/impl/android/AndroidNativeUtil.java
@@ -1,3 +1,25 @@
+/*
+ * 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.impl.android;
public final class AndroidNativeUtil {
From 3a372f73bf58f1b1aced54ea57faea30e7aa0573 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 06:14:09 +0300
Subject: [PATCH 03/14] Link SDK stub libraries in generated Apple projects
---
.../workflows/ad-cn1lib-ios-native-check.yml | 15 +--
scripts/check-admob-ios-link.sh | 93 +++++++++++++------
.../tools/translator/ByteCodeTranslator.java | 9 +-
.../BytecodeInstructionIntegrationTest.java | 15 ++-
4 files changed, 95 insertions(+), 37 deletions(-)
diff --git a/.github/workflows/ad-cn1lib-ios-native-check.yml b/.github/workflows/ad-cn1lib-ios-native-check.yml
index 0d61b1b62ce..5aeece40b47 100644
--- a/.github/workflows/ad-cn1lib-ios-native-check.yml
+++ b/.github/workflows/ad-cn1lib-ios-native-check.yml
@@ -21,9 +21,8 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'scripts/check-admob-ios-link.sh'
- - 'vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj'
- - 'vm/ByteCodeTranslator/src/cn1_globals.h'
- - 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
+ - 'vm/ByteCodeTranslator/**'
+ - 'vm/pom.xml'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
push:
branches: [master]
@@ -32,9 +31,8 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'scripts/check-admob-ios-link.sh'
- - 'vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj'
- - 'vm/ByteCodeTranslator/src/cn1_globals.h'
- - 'vm/ByteCodeTranslator/src/cn1_virtual_thread.h'
+ - 'vm/ByteCodeTranslator/**'
+ - 'vm/pom.xml'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
concurrency:
@@ -214,6 +212,9 @@ jobs:
sdk: [iphoneos, iphonesimulator]
steps:
- uses: actions/checkout@v6
- - run: brew install xcodegen
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '8'
- name: Link the native bridge and SDK into an app
run: scripts/check-admob-ios-link.sh "$RUNNER_TEMP/admob-link" "${{ matrix.sdk }}"
diff --git a/scripts/check-admob-ios-link.sh b/scripts/check-admob-ios-link.sh
index 683aafe02be..4f31758eaec 100755
--- a/scripts/check-admob-ios-link.sh
+++ b/scripts/check-admob-ios-link.sh
@@ -5,6 +5,8 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PROBE="${1:-$(mktemp -d /tmp/cn1-admob-link.XXXXXX)}"
SDK="${2:-iphoneos}"
+# Use Java 8, matching the translator build in CI.
+mvn -B -f "$ROOT/vm/pom.xml" -pl ByteCodeTranslator -am -DskipTests package
mkdir -p "$PROBE/Sources"
cp "$ROOT"/maven/cn1-admob/ios/src/main/objectivec/* "$PROBE/Sources/"
cp "$ROOT"/vm/ByteCodeTranslator/src/{cn1_globals.h,cn1_virtual_thread.h} "$PROBE/"
@@ -29,34 +31,73 @@ int main(int argc, char** argv) {
}
}
OBJC
-python3 - "$ROOT" "$PROBE" <<'PY'
-import json, pathlib, re, sys
-root, probe = map(pathlib.Path, sys.argv[1:])
+# Translate a minimal Java entry point, with the shipped library hint. The
+# resulting framework references and library search paths stay intact below.
+cat > "$PROBE/AdMobLinkProbe.java" <<'JAVA'
+public class AdMobLinkProbe {
+ public static void main(String[] args) {}
+}
+JAVA
+javac -d "$PROBE/Sources" "$PROBE/AdMobLinkProbe.java"
+LIBS="$(sed -n 's/^codename1\.arg\.ios\.add_libs=;*//p' \
+ "$ROOT/maven/cn1-admob/common/codenameone_library_appended.properties")"
+java -jar "$ROOT/vm/ByteCodeTranslator/dist/ByteCodeTranslator.jar" ios \
+ "$PROBE/Sources" "$PROBE/generated" AdMobLinkProbe com.codenameone.test \
+ AdMobLinkProbe 1.0 ios "${LIBS:-none}"
+PROJECT="$PROBE/generated/dist"
+cp "$PROBE"/{cn1_globals.h,cn1_virtual_thread.h,cn1_class_method_index.h,Prefix.pch} "$PROJECT/"
+plutil -convert json -o "$PROBE/project.json" "$PROJECT/AdMobLinkProbe.xcodeproj/project.pbxproj"
+python3 - "$ROOT" "$PROBE" "$PROJECT" "$LIBS" <<'PYTHON'
+import json, pathlib, plistlib, re, sys
+root, probe, project = map(pathlib.Path, sys.argv[1:4])
props = root / 'maven/cn1-admob/common'
pod = re.search(r'^codename1.arg.ios.pods=(.+)$', (props / 'codenameone_library_required.properties').read_text(), re.M).group(1)
name, version = pod.split(' ', 1)
-(probe / 'Podfile').write_text("platform :ios, '14.0'\ntarget 'AdMobLinkProbe' do\n use_frameworks!\n pod '%s', '%s'\nend\n" % (name, version))
-# Consume the same search paths and library hints as a generated CN1 app.
-template = (root / 'vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj').read_text()
-blocks = re.findall(r'LIBRARY_SEARCH_PATHS = \((.*?)\);', template, re.S)
-paths = re.findall(r'"([^"]+)"', blocks[0])
-assert all(re.findall(r'"([^"]+)"', block) == paths for block in blocks)
-paths = [p for p in paths if 'template-src' not in p]
-appended = (props / 'codenameone_library_appended.properties').read_text()
-match = re.search(r'^codename1.arg.ios.add_libs=(.*)$', appended, re.M)
-libs = [] if not match else [v for v in match.group(1).split(';') if v]
-settings = {'CLANG_ENABLE_MODULES': 'YES', 'CLANG_ENABLE_OBJC_ARC': 'NO',
- 'CODE_SIGNING_ALLOWED': 'NO', 'GENERATE_INFOPLIST_FILE': 'YES',
- 'GCC_PREFIX_HEADER': 'Prefix.pch', 'GCC_PRECOMPILE_PREFIX_HEADER': 'NO',
- 'HEADER_SEARCH_PATHS': ['$(inherited)', '$(SRCROOT)'],
- 'LIBRARY_SEARCH_PATHS': paths, 'OTHER_LDFLAGS': ['$(inherited)', '-ObjC'],
- 'DEAD_CODE_STRIPPING': 'NO'}
-project = {'name': 'AdMobLinkProbe', 'options': {'bundleIdPrefix': 'com.codenameone.test', 'deploymentTarget': {'iOS': '14.0'}},
- 'targets': {'AdMobLinkProbe': {'type': 'application', 'platform': 'iOS', 'sources': ['Sources'],
- 'dependencies': [{'sdk': lib} for lib in libs], 'settings': {'base': settings}}}}
-(probe / 'project.yml').write_text(json.dumps(project, indent=2))
-PY
-(cd "$PROBE" && xcodegen generate && pod install)
-xcodebuild -workspace "$PROBE/AdMobLinkProbe.xcworkspace" -scheme AdMobLinkProbe \
+(project / 'Podfile').write_text("platform :ios, '14.0'\ntarget 'AdMobLinkProbe' do\n use_frameworks!\n pod '%s', '%s'\nend\n" % (name, version))
+data = json.loads((probe / 'project.json').read_text())
+objects = data['objects']
+# A successful link alone is insufficient: other frameworks may supply C++
+# transitively. Verify the shipped hint is an explicit SDK library input before
+# trimming the application scaffolding.
+app = next(obj for obj in objects.values() if obj['isa'] == 'PBXNativeTarget' and obj['name'] == 'AdMobLinkProbe')
+frameworks = next(objects[ref] for ref in app['buildPhases'] if objects[ref]['isa'] == 'PBXFrameworksBuildPhase')
+linked = {objects[ref]['fileRef'] for ref in frameworks['files']}
+resources = next(objects[ref] for ref in app['buildPhases'] if objects[ref]['isa'] == 'PBXResourcesBuildPhase')
+copied = {objects[ref]['fileRef'] for ref in resources['files']}
+for lib in filter(None, sys.argv[4].split(';')):
+ if lib.endswith('.tbd'):
+ matches = [(ref, obj) for ref, obj in objects.items()
+ if obj['isa'] == 'PBXFileReference' and obj.get('name') == lib]
+ assert len(matches) == 1, 'Missing SDK library reference: ' + lib
+ ref, obj = matches[0]
+ assert obj.get('path') == 'usr/lib/' + lib and obj.get('sourceTree') == 'SDKROOT', obj
+ assert obj.get('lastKnownFileType') == 'sourcecode.text-based-dylib-definition', obj
+ assert ref in linked and ref not in copied, 'Library hint is not a linker input: ' + lib
+# Only replace translated runtime sources with the callback stubs above. Keep
+# the translator's Frameworks phase, SDK paths and LIBRARY_SEARCH_PATHS verbatim.
+source_names = {'main.m', 'com_codename1_ads_admob_AdMobNativeImpl.m'}
+for obj in objects.values():
+ if obj['isa'] == 'PBXSourcesBuildPhase':
+ obj['files'] = [ref for ref in obj['files']
+ if objects[objects[ref]['fileRef']].get('path') in source_names]
+ elif obj['isa'] == 'PBXResourcesBuildPhase':
+ obj['files'] = []
+ elif obj['isa'] == 'XCBuildConfiguration':
+ settings = obj['buildSettings']
+ settings.pop('INFOPLIST_FILE', None)
+ settings.update({'CLANG_ENABLE_MODULES': 'YES', 'CODE_SIGNING_ALLOWED': 'NO',
+ 'GENERATE_INFOPLIST_FILE': 'YES', 'IPHONEOS_DEPLOYMENT_TARGET': '14.0',
+ 'PRODUCT_BUNDLE_IDENTIFIER': 'com.codenameone.test.AdMobLinkProbe',
+ 'GCC_PREFIX_HEADER': 'Prefix.pch', 'GCC_PRECOMPILE_PREFIX_HEADER': 'NO',
+ 'HEADER_SEARCH_PATHS': ['$(inherited)', '$(SRCROOT)'],
+ 'OTHER_LDFLAGS': ['$(inherited)', '-ObjC'], 'DEAD_CODE_STRIPPING': 'NO'})
+# Fail explicitly if staging stopped including the real bridge or entry point.
+sources = next(objects[ref] for ref in app['buildPhases'] if objects[ref]['isa'] == 'PBXSourcesBuildPhase')
+assert len(sources['files']) == len(source_names), sources
+with (project / 'AdMobLinkProbe.xcodeproj/project.pbxproj').open('wb') as output:
+ plistlib.dump(data, output)
+PYTHON
+(cd "$PROJECT" && pod install)
+xcodebuild -workspace "$PROJECT/AdMobLinkProbe.xcworkspace" -scheme AdMobLinkProbe \
-configuration Release -sdk "$SDK" -derivedDataPath "$PROBE/build-$SDK" \
CODE_SIGNING_ALLOWED=NO build
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java
index 585249e0c2e..84ec56ae23b 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java
@@ -936,10 +936,10 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File
fileListEntry.append(file);
fileListEntry.append(" */ = {isa = PBXFileReference; lastKnownFileType = ");
fileListEntry.append(getFileType(file));
- if(file.endsWith(".framework") || file.endsWith(".dylib") || file.endsWith(".a")) {
+ if(file.endsWith(".framework") || file.endsWith(".dylib") || file.endsWith(".tbd") || file.endsWith(".a")) {
fileListEntry.append("; name = \"");
fileListEntry.append(file);
- if(file.endsWith(".dylib")) {
+ if(file.endsWith(".dylib") || file.endsWith(".tbd")) {
fileListEntry.append("\"; path = \"usr/lib/");
fileListEntry.append(file);
fileListEntry.append("\"; sourceTree = SDKROOT; };\n");
@@ -1021,7 +1021,7 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File
fileThreeEntry.append(" */,\n");
}
} else {
- if(file.endsWith(".a") || file.endsWith(".framework") || file.endsWith(".dylib") || (file.endsWith("Info.plist") && !"GoogleService-Info.plist".equals(file)) || file.endsWith(".pch")) {
+ if(file.endsWith(".a") || file.endsWith(".framework") || file.endsWith(".dylib") || file.endsWith(".tbd") || (file.endsWith("Info.plist") && !"GoogleService-Info.plist".equals(file)) || file.endsWith(".pch")) {
frameworks.append(" 0");
frameworks.append(referenceValue);
frameworks.append("18E9ABBC002F3D1D /* ");
@@ -1409,6 +1409,9 @@ private static String getFileType(String s) {
if(s.endsWith(".dylib")) {
return "compiled.mach-o.dylib";
}
+ if(s.endsWith(".tbd")) {
+ return "sourcecode.text-based-dylib-definition";
+ }
if(s.endsWith(".h")) {
return "sourcecode.c.h";
}
diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java
index 0e88b50b8aa..e3031959ee8 100644
--- a/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java
+++ b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java
@@ -1004,6 +1004,8 @@ void getFileTypeReturnsCorrectTypes() throws Exception {
m.setAccessible(true);
assertEquals("wrapper.framework", m.invoke(null, "foo.framework"));
+ assertEquals("compiled.mach-o.dylib", m.invoke(null, "libz.dylib"));
+ assertEquals("sourcecode.text-based-dylib-definition", m.invoke(null, "libc++.tbd"));
assertEquals("sourcecode.c.objc", m.invoke(null, "foo.m"));
assertEquals("file", m.invoke(null, "foo.txt"));
assertEquals("wrapper.plug-in", m.invoke(null, "foo.bundle"));
@@ -1049,7 +1051,7 @@ void handleIosOutputGeneratesProjectStructure(CompilerHelper.CompilerConfig conf
"ios",
sourceDir.toAbsolutePath().toString(),
outputDir.toAbsolutePath().toString(),
- "MyAppIOS", "com.example", "My App", "1.0", "ios", "none"
+ "MyAppIOS", "com.example", "My App", "1.0", "ios", "libc++.tbd"
};
ByteCodeTranslator.OutputType originalOutput = ByteCodeTranslator.output;
@@ -1069,6 +1071,17 @@ void handleIosOutputGeneratesProjectStructure(CompilerHelper.CompilerConfig conf
assertTrue(pbxproj.contains("CoreText.framework"),
"iOS projects must link CoreText for IOSNative bundled font registration");
+ String stubReference = fileReferenceLine(pbxproj, "libc++.tbd");
+ assertTrue(stubReference.contains("lastKnownFileType = sourcecode.text-based-dylib-definition"));
+ assertTrue(stubReference.contains("path = \"usr/lib/libc++.tbd\""));
+ assertTrue(stubReference.contains("sourceTree = SDKROOT"));
+ assertTrue(buildPhase(pbxproj, "PBXFrameworksBuildPhase").contains("libc++.tbd"),
+ "SDK library stubs must be linked into the app");
+ assertFalse(buildPhase(pbxproj, "PBXResourcesBuildPhase").contains("libc++.tbd"),
+ "SDK library stubs must not be copied as application resources");
+ assertTrue(buildPhase(pbxproj, "PBXFrameworksBuildPhase").contains("libz.dylib"),
+ "legacy dylib hints must continue to link");
+
// The assembly file must be typed AND filed as a source. An extension
// Xcode does not recognise gets `lastKnownFileType = file` and lands in
// the Resources phase, where it is copied into the bundle and never
From 7fe28bc8f3187279e2e38e8e7c3903ad0bed8abb Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 07:11:50 +0300
Subject: [PATCH 04/14] Use Zulu Java 8 for ARM64 macOS AdMob checks
---
.github/workflows/ad-cn1lib-ios-native-check.yml | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ad-cn1lib-ios-native-check.yml b/.github/workflows/ad-cn1lib-ios-native-check.yml
index 5aeece40b47..d1d282330dc 100644
--- a/.github/workflows/ad-cn1lib-ios-native-check.yml
+++ b/.github/workflows/ad-cn1lib-ios-native-check.yml
@@ -212,9 +212,10 @@ jobs:
sdk: [iphoneos, iphonesimulator]
steps:
- uses: actions/checkout@v6
- - uses: actions/setup-java@v4
+ # Temurin 8 is unavailable on macOS ARM64; Zulu supplies native Java 8.
+ - uses: actions/setup-java@v5
with:
- distribution: temurin
+ distribution: zulu
java-version: '8'
- name: Link the native bridge and SDK into an app
run: scripts/check-admob-ios-link.sh "$RUNNER_TEMP/admob-link" "${{ matrix.sdk }}"
From a0523d64c044bc353b41ef3821b8dc660f009cb7 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 09:25:16 +0300
Subject: [PATCH 05/14] Handle mock ad dismissal from dialogs and app startup
---
.../codename1/ads/mock/MockAdProvider.java | 13 ++-
.../com/codename1/ads/MockAdProviderTest.java | 94 +++++++++++++++++++
2 files changed, 106 insertions(+), 1 deletion(-)
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index 8627413f4b8..818a6cef0a9 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -43,6 +43,7 @@
import com.codename1.ui.Command;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
+import com.codename1.ui.Dialog;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.events.ActionEvent;
@@ -159,6 +160,10 @@ public void show() {
}
loaded = false;
previousForm = CN.getCurrentForm();
+ if (previousForm == null) {
+ // App-open ads may be presented before the first application form.
+ previousForm = new Form();
+ }
adForm = new Form("Mock advertisement", new BorderLayout());
adForm.add(BorderLayout.CENTER, new Label("Advertisement"));
Button close = new Button("Close ad");
@@ -187,7 +192,13 @@ private void closeAd(boolean notify) {
adForm = null;
previousForm = null;
if (previous != null && CN.getCurrentForm() == closing) {
- previous.showBack();
+ if (previous instanceof Dialog) {
+ // showBack() would enter another modal wait and prevent the
+ // reward/dismissal callbacks from disposing the caller dialog.
+ ((Dialog) previous).showModeless();
+ } else {
+ previous.showBack();
+ }
}
if (notify) {
if (format == AdFormat.REWARDED || format == AdFormat.REWARDED_INTERSTITIAL) {
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index a3b6e892c16..91a3f1bc663 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -27,11 +27,15 @@
import com.codename1.junit.UITestBase;
import com.codename1.ui.Button;
import com.codename1.ui.CN;
+import com.codename1.ui.Dialog;
import com.codename1.ui.Form;
import com.codename1.ui.events.ActionEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
class MockAdProviderTest extends UITestBase {
@@ -100,4 +104,94 @@ void disposingVisibleAdRestoresFormWithoutRewardOrDismissal() {
assertSame(previous, CN.getCurrentForm());
assertEquals(Arrays.asList("shown", "impression"), events);
}
+
+ @FormTest
+ void modalDialogCanBeDisposedAndNavigatedAwayFromInDismissalCallback() {
+ MockAdProvider.install();
+ Dialog dialog = new Dialog("Modal caller");
+ Form destination = new Form("After dismissal");
+ RewardedAd ad = new RewardedAd("mock");
+ AtomicReference failure = new AtomicReference();
+ Timer watchdog = new Timer(true);
+ boolean[] started = {false};
+ ad.setAdListener(new AdListener() {
+ @Override public void onDismissed() {
+ events.add("dismissed");
+ assertSame(dialog, CN.getCurrentForm());
+ dialog.dispose();
+ destination.show();
+ }
+ });
+ ad.setOnUserEarnedRewardListener(reward -> events.add("reward"));
+ dialog.addShowListener(evt -> {
+ if (started[0]) {
+ return;
+ }
+ started[0] = true;
+ CN.callSerially(() -> {
+ try {
+ ad.load();
+ ad.show();
+ Form showing = CN.getCurrentForm();
+ // Unblock the broken implementation so failure is an assertion,
+ // not a hung EDT. The fixed close delivers callbacks immediately.
+ watchdog.schedule(new TimerTask() {
+ @Override public void run() {
+ CN.callSerially(() -> {
+ if (!events.contains("dismissed")) {
+ events.add("watchdog");
+ dialog.dispose();
+ }
+ });
+ }
+ }, 1000);
+ showing.getBackCommand().actionPerformed(new ActionEvent(showing));
+ } catch (Throwable t) {
+ failure.set(t);
+ dialog.dispose();
+ }
+ });
+ });
+ try {
+ dialog.show();
+ assertNull(failure.get(), "Dismissal callback must see the restored dialog");
+ assertEquals(Arrays.asList("reward", "dismissed"), events);
+ assertSame(destination, CN.getCurrentForm());
+ } finally {
+ watchdog.cancel();
+ ad.dispose();
+ dialog.dispose();
+ }
+ }
+
+ @FormTest
+ void appOpenAdWithoutPreviousFormCanCloseGoBackOrDispose() {
+ MockAdProvider.install();
+ for (int action = 0; action < 3; action++) {
+ events.clear();
+ implementation.setCurrentForm(null);
+ assertNull(CN.getCurrentForm());
+ AppOpenAd ad = new AppOpenAd("mock");
+ ad.setAdListener(listener());
+ ad.load();
+ ad.show();
+ Form showing = CN.getCurrentForm();
+ assertEquals("Mock advertisement", showing.getTitle());
+ if (action == 0) {
+ Button close = (Button) showing.getContentPane().getComponentAt(1);
+ close.pressed();
+ close.released();
+ } else if (action == 1) {
+ showing.getBackCommand().actionPerformed(new ActionEvent(showing));
+ } else {
+ ad.dispose();
+ }
+ assertNotNull(CN.getCurrentForm());
+ assertNotSame(showing, CN.getCurrentForm(), "Dismissal must remove the ad even at startup");
+ assertEquals(action == 2 ? Arrays.asList("shown", "impression")
+ : Arrays.asList("shown", "impression", "dismissed"), events);
+ ad.dispose();
+ }
+ }
+
}
From d410c967635f8616497d3e5b8f62d3ba62cafcff Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 10:40:45 +0300
Subject: [PATCH 06/14] Preserve caller dialog modality when dismissing mock
ads
---
.../codename1/ads/mock/MockAdProvider.java | 34 +++++---------
.../com/codename1/ads/MockAdProviderTest.java | 47 +++++++++++++++++++
2 files changed, 60 insertions(+), 21 deletions(-)
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index 818a6cef0a9..aa35cf29631 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -44,7 +44,6 @@
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.Dialog;
-import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.geom.Dimension;
@@ -121,8 +120,7 @@ private static final class MockFullScreen implements FullScreenAdSession {
private final AdFormat format;
private AdSessionCallback cb;
private boolean loaded;
- private Form adForm;
- private Form previousForm;
+ private Dialog adForm;
MockFullScreen(AdFormat format) {
this.format = format;
@@ -159,12 +157,12 @@ public void show() {
return;
}
loaded = false;
- previousForm = CN.getCurrentForm();
- if (previousForm == null) {
- // App-open ads may be presented before the first application form.
- previousForm = new Form();
- }
- adForm = new Form("Mock advertisement", new BorderLayout());
+ // Dialog disposal restores its caller directly, without re-showing a
+ // modal caller or changing how that caller behaves on later shows.
+ // It also supplies a fallback form when presented during app startup.
+ adForm = new Dialog("Mock advertisement", new BorderLayout());
+ adForm.setNativeWindowMode(false);
+ adForm.setAutoDispose(false);
adForm.add(BorderLayout.CENTER, new Label("Advertisement"));
Button close = new Button("Close ad");
close.addActionListener(evt -> closeAd(true));
@@ -175,7 +173,7 @@ public void actionPerformed(ActionEvent evt) {
closeAd(true);
}
});
- adForm.show();
+ adForm.showAtPosition(0, 0, 0, 0, false);
cb.onShown();
// A listener may dispose the ad synchronously from onShown().
if (adForm != null) {
@@ -187,19 +185,13 @@ private void closeAd(boolean notify) {
if (adForm == null) {
return;
}
- Form closing = adForm;
- Form previous = previousForm;
+ Dialog closing = adForm;
adForm = null;
- previousForm = null;
- if (previous != null && CN.getCurrentForm() == closing) {
- if (previous instanceof Dialog) {
- // showBack() would enter another modal wait and prevent the
- // reward/dismissal callbacks from disposing the caller dialog.
- ((Dialog) previous).showModeless();
- } else {
- previous.showBack();
- }
+ if (CN.getCurrentForm() != closing) {
+ // Application code may already have navigated away from the ad.
+ closing.setPreviousForm(null);
}
+ closing.dispose();
if (notify) {
if (format == AdFormat.REWARDED || format == AdFormat.REWARDED_INTERSTITIAL) {
cb.onUserEarnedReward(new RewardItem("coins", 10));
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index 91a3f1bc663..7872e1e6267 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -157,6 +157,16 @@ void modalDialogCanBeDisposedAndNavigatedAwayFromInDismissalCallback() {
assertNull(failure.get(), "Dismissal callback must see the restored dialog");
assertEquals(Arrays.asList("reward", "dismissed"), events);
assertSame(destination, CN.getCurrentForm());
+ // Reusing the same caller must still enter its original modal wait.
+ // Queue disposal from onShow so only the modal event loop can run it
+ // before show() returns.
+ boolean[] disposedDuringShow = {false};
+ dialog.addShowListener(evt -> CN.callSerially(() -> {
+ disposedDuringShow[0] = true;
+ dialog.dispose();
+ }));
+ dialog.show();
+ assertTrue(disposedDuringShow[0], "A reused modal caller must still block until disposed");
} finally {
watchdog.cancel();
ad.dispose();
@@ -194,4 +204,41 @@ void appOpenAdWithoutPreviousFormCanCloseGoBackOrDispose() {
}
}
+ @FormTest
+ void modelessCallerStaysModelessAfterAdDisposal() {
+ MockAdProvider.install();
+ Dialog caller = new Dialog("Modeless caller");
+ caller.showModeless();
+ InterstitialAd ad = new InterstitialAd("mock");
+ try {
+ ad.load();
+ ad.show();
+ ad.dispose();
+ assertSame(caller, CN.getCurrentForm());
+ caller.dispose();
+ boolean[] disposedDuringShow = {false};
+ caller.addShowListener(evt -> CN.callSerially(() -> {
+ disposedDuringShow[0] = true;
+ caller.dispose();
+ }));
+ caller.show();
+ assertFalse(disposedDuringShow[0], "A reused modeless caller must not wait for disposal");
+ } finally {
+ ad.dispose();
+ caller.dispose();
+ }
+ }
+
+ @FormTest
+ void disposalDoesNotUndoApplicationNavigation() {
+ MockAdProvider.install();
+ InterstitialAd ad = new InterstitialAd("mock");
+ ad.load();
+ ad.show();
+ Form destination = new Form("Application destination");
+ destination.show();
+ ad.dispose();
+ assertSame(destination, CN.getCurrentForm());
+ }
+
}
From 5aa45fb5afde03b0c3f9ef9456f37c3e543a782e Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 11:00:42 +0300
Subject: [PATCH 07/14] Preserve nested dialog navigation when disposing mock
ads
---
CodenameOne/src/com/codename1/ui/Dialog.java | 15 +++++
.../codename1/ads/mock/MockAdProvider.java | 11 +++-
.../com/codename1/ads/MockAdProviderTest.java | 59 +++++++++++++++++++
3 files changed, 83 insertions(+), 2 deletions(-)
diff --git a/CodenameOne/src/com/codename1/ui/Dialog.java b/CodenameOne/src/com/codename1/ui/Dialog.java
index 86fbf13f67e..0b0a079e07c 100644
--- a/CodenameOne/src/com/codename1/ui/Dialog.java
+++ b/CodenameOne/src/com/codename1/ui/Dialog.java
@@ -1187,6 +1187,21 @@ private void initImpl(String dialogUIID, String dialogTitleUIID, Layout lm) {
deregisterAnimated(this);
}
+ /// Returns the form beneath this dialog in the navigation stack. This may be
+ /// another dialog when dialogs are nested.
+ ///
+ /// #### Returns
+ ///
+ /// the previous form, or null when no previous form is associated with this dialog
+ ///
+ /// #### See also
+ ///
+ /// - `#setPreviousForm(Form)`
+ @Override
+ public Form getPreviousForm() {
+ return super.getPreviousForm();
+ }
+
/// When the dialog is disposed this form will show. Notice that this can only be set after show was invoked!
///
/// #### Parameters
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index aa35cf29631..002ba1cd48c 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -44,6 +44,7 @@
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.Dialog;
+import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.geom.Dimension;
@@ -187,8 +188,14 @@ private void closeAd(boolean notify) {
}
Dialog closing = adForm;
adForm = null;
- if (CN.getCurrentForm() != closing) {
- // Application code may already have navigated away from the ad.
+ Form current = CN.getCurrentForm();
+ while (current instanceof Dialog && current != closing) {
+ current = ((Dialog) current).getPreviousForm();
+ }
+ if (current != closing) {
+ // Suppress restoration only after unrelated navigation. If an
+ // upper dialog still points to the ad, dispose() needs this link
+ // to splice the ad out of the active dialog stack.
closing.setPreviousForm(null);
}
closing.dispose();
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index 7872e1e6267..4ec6a4fee1d 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -241,4 +241,63 @@ void disposalDoesNotUndoApplicationNavigation() {
assertSame(destination, CN.getCurrentForm());
}
+ @FormTest
+ void disposalSplicesAdOutOfNestedDialogs() {
+ MockAdProvider.install();
+ Form application = CN.getCurrentForm();
+ for (int depth = 1; depth <= 2; depth++) {
+ events.clear();
+ InterstitialAd ad = new InterstitialAd("mock");
+ ad.setAdListener(listener());
+ Dialog[] overlays = new Dialog[depth];
+ try {
+ ad.load();
+ ad.show();
+ for (int i = 0; i < depth; i++) {
+ overlays[i] = new Dialog("Application overlay " + i);
+ overlays[i].showModeless();
+ }
+ ad.dispose();
+ ad.dispose();
+ assertSame(overlays[depth - 1], CN.getCurrentForm(),
+ "Disposing an underlying ad must leave the top dialog visible");
+ assertEquals(Arrays.asList("shown", "impression"), events);
+ for (int i = depth - 1; i >= 0; i--) {
+ overlays[i].dispose();
+ assertSame(i == 0 ? application : overlays[i - 1], CN.getCurrentForm(),
+ "Dialog disposal must skip the removed ad and restore its caller");
+ }
+ } finally {
+ for (int i = depth - 1; i >= 0; i--) {
+ if (overlays[i] != null) {
+ overlays[i].dispose();
+ }
+ }
+ ad.dispose();
+ application.show();
+ }
+ }
+ }
+
+ @FormTest
+ void disposalDoesNotUndoNavigationToAnUnrelatedDialog() {
+ MockAdProvider.install();
+ InterstitialAd ad = new InterstitialAd("mock");
+ Form destination = new Form("Application destination");
+ Dialog unrelated = new Dialog("Unrelated dialog");
+ try {
+ ad.load();
+ ad.show();
+ destination.show();
+ unrelated.showModeless();
+ ad.dispose();
+ assertSame(unrelated, CN.getCurrentForm());
+ unrelated.dispose();
+ assertSame(destination, CN.getCurrentForm());
+ } finally {
+ ad.dispose();
+ unrelated.dispose();
+ }
+ }
+
}
From 15fcd1917a96346d45783f9dd9d8344b53e056fb Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 11:21:02 +0300
Subject: [PATCH 08/14] Serialize mock ad lifecycle transitions on the EDT
---
.../codename1/ads/mock/MockAdProvider.java | 23 ++++-
.../com/codename1/ads/MockAdProviderTest.java | 88 +++++++++++++++++++
2 files changed, 110 insertions(+), 1 deletion(-)
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index 002ba1cd48c..d43a48d9734 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -120,7 +120,9 @@ public void loadNativeAd(String adUnitId, AdRequest request,
private static final class MockFullScreen implements FullScreenAdSession {
private final AdFormat format;
private AdSessionCallback cb;
- private boolean loaded;
+ // Mutated on the EDT; readiness may be queried by a worker.
+ private volatile boolean loaded;
+ private boolean disposed;
private Dialog adForm;
MockFullScreen(AdFormat format) {
@@ -138,6 +140,13 @@ public void setServerSideVerificationOptions(ServerSideVerificationOptions optio
@Override
public void load(AdRequest request) {
+ if (!CN.isEdt()) {
+ CN.callSerially(() -> load(request));
+ return;
+ }
+ if (disposed) {
+ return;
+ }
loaded = true;
cb.onLoaded();
}
@@ -149,6 +158,13 @@ public boolean isLoaded() {
@Override
public void show() {
+ if (!CN.isEdt()) {
+ CN.callSerially(this::show);
+ return;
+ }
+ if (disposed) {
+ return;
+ }
if (!loaded) {
cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "No ad loaded"));
return;
@@ -213,6 +229,11 @@ public void setAutoShowOnForeground(boolean enabled) {
@Override
public void dispose() {
+ if (!CN.isEdt()) {
+ CN.callSerially(this::dispose);
+ return;
+ }
+ disposed = true;
loaded = false;
closeAd(false);
}
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index 4ec6a4fee1d..86519805f3b 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -36,6 +36,7 @@
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.CountDownLatch;
import static org.junit.jupiter.api.Assertions.*;
class MockAdProviderTest extends UITestBase {
@@ -300,4 +301,91 @@ void disposalDoesNotUndoNavigationToAnUnrelatedDialog() {
}
}
+ @FormTest
+ void workerShowThenDisposeDoesNotStrandQueuedDialog() throws Exception {
+ MockAdProvider.install();
+ Form previous = CN.getCurrentForm();
+ InterstitialAd ad = new InterstitialAd("mock");
+ ad.setAdListener(listener());
+ AtomicReference failure = new AtomicReference();
+ Thread worker = new Thread(() -> {
+ try {
+ ad.load();
+ ad.show();
+ ad.dispose();
+ } catch (Throwable t) {
+ failure.set(t);
+ }
+ }, "mock-ad-worker");
+ try {
+ // Keep the EDT here until the worker has queued both operations.
+ // invokeAndBlock would pump the queue and hide the failing ordering.
+ worker.start();
+ worker.join(1000);
+ assertFalse(worker.isAlive(), "Worker operations must not wait for the EDT");
+ assertNull(failure.get());
+ CountDownLatch drained = new CountDownLatch(1);
+ CN.callSerially(drained::countDown);
+ waitFor(drained, 1000);
+ assertSame(previous, CN.getCurrentForm(), "Queued presentation must not resurrect a disposed ad");
+ assertEquals(Arrays.asList("shown", "impression"), events);
+ } finally {
+ ad.dispose();
+ previous.show();
+ }
+ }
+
+ @FormTest
+ void onShownCanDisposeSynchronouslyWithoutAnImpression() {
+ MockAdProvider.install();
+ Form previous = CN.getCurrentForm();
+ InterstitialAd ad = new InterstitialAd("mock");
+ ad.setAdListener(new AdListener() {
+ @Override public void onShown() {
+ events.add("shown");
+ assertTrue(CN.isEdt());
+ assertNotSame(previous, CN.getCurrentForm());
+ ad.dispose();
+ }
+ @Override public void onImpression() { events.add("impression"); }
+ });
+ ad.load();
+ ad.show();
+ assertSame(previous, CN.getCurrentForm());
+ assertEquals(Arrays.asList("shown"), events);
+ }
+
+ @FormTest
+ void edtDisposalCancelsAlreadyQueuedWorkerPresentation() throws Exception {
+ MockAdProvider.install();
+ Form previous = CN.getCurrentForm();
+ InterstitialAd ad = new InterstitialAd("mock");
+ ad.setAdListener(listener());
+ AtomicReference failure = new AtomicReference();
+ Thread worker = new Thread(() -> {
+ try {
+ ad.load();
+ ad.show();
+ } catch (Throwable t) {
+ failure.set(t);
+ }
+ }, "mock-ad-queued-presentation");
+ try {
+ worker.start();
+ worker.join(1000);
+ assertFalse(worker.isAlive());
+ assertNull(failure.get());
+ // An EDT caller can dispose before the queued load/show get a turn.
+ ad.dispose();
+ CountDownLatch drained = new CountDownLatch(1);
+ CN.callSerially(drained::countDown);
+ waitFor(drained, 1000);
+ assertSame(previous, CN.getCurrentForm());
+ assertTrue(events.isEmpty(), "A disposed session must ignore pending presentation");
+ } finally {
+ ad.dispose();
+ previous.show();
+ }
+ }
+
}
From 69c8e7c690668e18e74e0957675b670a90cf0030 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 12:09:47 +0300
Subject: [PATCH 09/14] Simplify mock full-screen ads to layered-pane overlays
---
CodenameOne/src/com/codename1/ui/Dialog.java | 15 ---
.../codename1/ads/mock/MockAdProvider.java | 68 ++++++-----
.../com/codename1/ads/MockAdProviderTest.java | 111 ++++++++----------
3 files changed, 92 insertions(+), 102 deletions(-)
diff --git a/CodenameOne/src/com/codename1/ui/Dialog.java b/CodenameOne/src/com/codename1/ui/Dialog.java
index 0b0a079e07c..86fbf13f67e 100644
--- a/CodenameOne/src/com/codename1/ui/Dialog.java
+++ b/CodenameOne/src/com/codename1/ui/Dialog.java
@@ -1187,21 +1187,6 @@ private void initImpl(String dialogUIID, String dialogTitleUIID, Layout lm) {
deregisterAnimated(this);
}
- /// Returns the form beneath this dialog in the navigation stack. This may be
- /// another dialog when dialogs are nested.
- ///
- /// #### Returns
- ///
- /// the previous form, or null when no previous form is associated with this dialog
- ///
- /// #### See also
- ///
- /// - `#setPreviousForm(Form)`
- @Override
- public Form getPreviousForm() {
- return super.getPreviousForm();
- }
-
/// When the dialog is disposed this form will show. Notice that this can only be set after show was invoked!
///
/// #### Parameters
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index d43a48d9734..72add5ae799 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -43,12 +43,12 @@
import com.codename1.ui.Command;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
-import com.codename1.ui.Dialog;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.layouts.BorderLayout;
+import com.codename1.ui.layouts.LayeredLayout;
/// A deterministic, network-free ad provider for tests and screenshots. It
/// renders fixed, labelled "ads" with stable colours, text and sizes (no
@@ -116,14 +116,18 @@ public void loadNativeAd(String adUnitId, AdRequest request,
}
/// Deterministic full screen ad: fires the lifecycle events and presents a
- /// fixed close-able form.
+ /// full-screen overlay with a Close button.
private static final class MockFullScreen implements FullScreenAdSession {
private final AdFormat format;
private AdSessionCallback cb;
// Mutated on the EDT; readiness may be queried by a worker.
private volatile boolean loaded;
private boolean disposed;
- private Dialog adForm;
+ private Container overlay;
+ private Container layer;
+ private Form host;
+ private Command previousBack;
+ private Command closeCommand;
MockFullScreen(AdFormat format) {
this.format = format;
@@ -169,52 +173,60 @@ public void show() {
cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "No ad loaded"));
return;
}
- if (adForm != null) {
+ if (overlay != null) {
cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "An ad is already showing"));
return;
}
loaded = false;
- // Dialog disposal restores its caller directly, without re-showing a
- // modal caller or changing how that caller behaves on later shows.
- // It also supplies a fallback form when presented during app startup.
- adForm = new Dialog("Mock advertisement", new BorderLayout());
- adForm.setNativeWindowMode(false);
- adForm.setAutoDispose(false);
- adForm.add(BorderLayout.CENTER, new Label("Advertisement"));
+ host = CN.getCurrentForm();
+ if (host == null) {
+ host = new Form();
+ host.show();
+ }
+ overlay = new Container(new BorderLayout());
+ overlay.setGrabsPointerEvents(true);
+ overlay.setUIID("Form");
+ overlay.getAllStyles().setBgTransparency(255);
+ overlay.add(BorderLayout.CENTER, new Label("Mock advertisement"));
Button close = new Button("Close ad");
close.addActionListener(evt -> closeAd(true));
- adForm.add(BorderLayout.SOUTH, close);
- adForm.setBackCommand(new Command("Close ad") {
+ overlay.add(BorderLayout.SOUTH, close);
+ previousBack = host.getBackCommand();
+ closeCommand = new Command("Close ad") {
@Override
public void actionPerformed(ActionEvent evt) {
closeAd(true);
}
- });
- adForm.showAtPosition(0, 0, 0, 0, false);
+ };
+ host.setBackCommand(closeCommand);
+ layer = host.getFormLayeredPane(MockAdProvider.class, true);
+ layer.setLayout(new LayeredLayout());
+ layer.add(overlay);
+ host.revalidate();
cb.onShown();
// A listener may dispose the ad synchronously from onShown().
- if (adForm != null) {
+ if (overlay != null) {
cb.onImpression();
}
}
private void closeAd(boolean notify) {
- if (adForm == null) {
+ if (overlay == null) {
return;
}
- Dialog closing = adForm;
- adForm = null;
- Form current = CN.getCurrentForm();
- while (current instanceof Dialog && current != closing) {
- current = ((Dialog) current).getPreviousForm();
+ overlay.remove();
+ overlay = null;
+ if (layer.getComponentCount() == 0) {
+ layer.remove();
}
- if (current != closing) {
- // Suppress restoration only after unrelated navigation. If an
- // upper dialog still points to the ad, dispose() needs this link
- // to splice the ad out of the active dialog stack.
- closing.setPreviousForm(null);
+ if (host.getBackCommand() == closeCommand) {
+ host.setBackCommand(previousBack);
}
- closing.dispose();
+ host.revalidate();
+ layer = null;
+ host = null;
+ previousBack = null;
+ closeCommand = null;
if (notify) {
if (format == AdFormat.REWARDED || format == AdFormat.REWARDED_INTERSTITIAL) {
cb.onUserEarnedReward(new RewardItem("coins", 10));
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index 86519805f3b..442890374ea 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -26,6 +26,9 @@
import com.codename1.junit.FormTest;
import com.codename1.junit.UITestBase;
import com.codename1.ui.Button;
+import com.codename1.ui.Command;
+import com.codename1.ui.Container;
+import com.codename1.ui.Label;
import com.codename1.ui.CN;
import com.codename1.ui.Dialog;
import com.codename1.ui.Form;
@@ -33,8 +36,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.Timer;
-import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.CountDownLatch;
import static org.junit.jupiter.api.Assertions.*;
@@ -45,6 +46,18 @@ class MockAdProviderTest extends UITestBase {
@org.junit.jupiter.api.BeforeEach
void resetEvents() { events.clear(); }
+ private Container adLayer(Form form) {
+ return form.getFormLayeredPane(MockAdProvider.class, true);
+ }
+
+ private Container adOverlay(Form form) {
+ return (Container) adLayer(form).getComponentAt(0);
+ }
+
+ private void assertNoAd(Form form) {
+ assertEquals(0, adLayer(form).getComponentCount());
+ }
+
private AdListener listener() {
return new AdListener() {
@Override public void onShown() { events.add("shown"); }
@@ -54,23 +67,30 @@ private AdListener listener() {
}
@FormTest
- void interstitialStaysVisibleUntilClosedAndRestoresPreviousForm() {
+ void interstitialOverlayStaysVisibleUntilClosed() {
Form previous = CN.getCurrentForm();
+ Command previousBack = new Command("Application back");
+ previous.setBackCommand(previousBack);
MockAdProvider.install();
InterstitialAd ad = new InterstitialAd("mock");
ad.setAdListener(listener());
ad.load();
ad.show();
Form showing = CN.getCurrentForm();
- assertNotSame(previous, showing);
- assertEquals("Mock advertisement", showing.getTitle());
+ assertSame(previous, showing);
+ assertEquals(showing.getWidth(), adOverlay(showing).getWidth());
+ assertEquals(showing.getHeight(), adOverlay(showing).getHeight());
+ assertEquals("Mock advertisement", ((Label) adOverlay(showing).getComponentAt(0)).getText());
assertEquals(Arrays.asList("shown", "impression"), events);
- Button close = (Button) showing.getContentPane().getComponentAt(1);
+ Command closeCommand = showing.getBackCommand();
+ Button close = (Button) adOverlay(showing).getComponentAt(1);
close.pressed();
close.released();
assertSame(previous, CN.getCurrentForm());
assertEquals(Arrays.asList("shown", "impression", "dismissed"), events);
- showing.getBackCommand().actionPerformed(new ActionEvent(showing));
+ closeCommand.actionPerformed(new ActionEvent(showing));
+ assertNoAd(showing);
+ assertSame(previousBack, showing.getBackCommand());
assertEquals(3, events.size(), "Closing twice must not repeat callbacks");
ad.dispose();
}
@@ -107,71 +127,40 @@ void disposingVisibleAdRestoresFormWithoutRewardOrDismissal() {
}
@FormTest
- void modalDialogCanBeDisposedAndNavigatedAwayFromInDismissalCallback() {
+ void overlayLeavesModalCallerAvailableToDismissalCallback() {
MockAdProvider.install();
Dialog dialog = new Dialog("Modal caller");
Form destination = new Form("After dismissal");
RewardedAd ad = new RewardedAd("mock");
AtomicReference failure = new AtomicReference();
- Timer watchdog = new Timer(true);
- boolean[] started = {false};
ad.setAdListener(new AdListener() {
@Override public void onDismissed() {
events.add("dismissed");
assertSame(dialog, CN.getCurrentForm());
+ assertNoAd(dialog);
dialog.dispose();
destination.show();
}
});
ad.setOnUserEarnedRewardListener(reward -> events.add("reward"));
- dialog.addShowListener(evt -> {
- if (started[0]) {
- return;
+ dialog.addShowListener(evt -> CN.callSerially(() -> {
+ try {
+ ad.load();
+ ad.show();
+ dialog.getBackCommand().actionPerformed(new ActionEvent(dialog));
+ } catch (Throwable t) {
+ failure.set(t);
+ } finally {
+ dialog.dispose();
}
- started[0] = true;
- CN.callSerially(() -> {
- try {
- ad.load();
- ad.show();
- Form showing = CN.getCurrentForm();
- // Unblock the broken implementation so failure is an assertion,
- // not a hung EDT. The fixed close delivers callbacks immediately.
- watchdog.schedule(new TimerTask() {
- @Override public void run() {
- CN.callSerially(() -> {
- if (!events.contains("dismissed")) {
- events.add("watchdog");
- dialog.dispose();
- }
- });
- }
- }, 1000);
- showing.getBackCommand().actionPerformed(new ActionEvent(showing));
- } catch (Throwable t) {
- failure.set(t);
- dialog.dispose();
- }
- });
- });
+ }));
try {
dialog.show();
- assertNull(failure.get(), "Dismissal callback must see the restored dialog");
+ assertNull(failure.get());
assertEquals(Arrays.asList("reward", "dismissed"), events);
assertSame(destination, CN.getCurrentForm());
- // Reusing the same caller must still enter its original modal wait.
- // Queue disposal from onShow so only the modal event loop can run it
- // before show() returns.
- boolean[] disposedDuringShow = {false};
- dialog.addShowListener(evt -> CN.callSerially(() -> {
- disposedDuringShow[0] = true;
- dialog.dispose();
- }));
- dialog.show();
- assertTrue(disposedDuringShow[0], "A reused modal caller must still block until disposed");
} finally {
- watchdog.cancel();
ad.dispose();
- dialog.dispose();
}
}
@@ -187,9 +176,9 @@ void appOpenAdWithoutPreviousFormCanCloseGoBackOrDispose() {
ad.load();
ad.show();
Form showing = CN.getCurrentForm();
- assertEquals("Mock advertisement", showing.getTitle());
+ assertEquals("Mock advertisement", ((Label) adOverlay(showing).getComponentAt(0)).getText());
if (action == 0) {
- Button close = (Button) showing.getContentPane().getComponentAt(1);
+ Button close = (Button) adOverlay(showing).getComponentAt(1);
close.pressed();
close.released();
} else if (action == 1) {
@@ -198,7 +187,8 @@ void appOpenAdWithoutPreviousFormCanCloseGoBackOrDispose() {
ad.dispose();
}
assertNotNull(CN.getCurrentForm());
- assertNotSame(showing, CN.getCurrentForm(), "Dismissal must remove the ad even at startup");
+ assertSame(showing, CN.getCurrentForm());
+ assertNoAd(showing);
assertEquals(action == 2 ? Arrays.asList("shown", "impression")
: Arrays.asList("shown", "impression", "dismissed"), events);
ad.dispose();
@@ -243,7 +233,7 @@ void disposalDoesNotUndoApplicationNavigation() {
}
@FormTest
- void disposalSplicesAdOutOfNestedDialogs() {
+ void disposalRemovesOverlayWithoutChangingNestedDialogs() {
MockAdProvider.install();
Form application = CN.getCurrentForm();
for (int depth = 1; depth <= 2; depth++) {
@@ -266,7 +256,7 @@ void disposalSplicesAdOutOfNestedDialogs() {
for (int i = depth - 1; i >= 0; i--) {
overlays[i].dispose();
assertSame(i == 0 ? application : overlays[i - 1], CN.getCurrentForm(),
- "Dialog disposal must skip the removed ad and restore its caller");
+ "Dialog disposal must restore its original caller");
}
} finally {
for (int i = depth - 1; i >= 0; i--) {
@@ -302,7 +292,7 @@ void disposalDoesNotUndoNavigationToAnUnrelatedDialog() {
}
@FormTest
- void workerShowThenDisposeDoesNotStrandQueuedDialog() throws Exception {
+ void workerShowThenDisposeRemovesQueuedOverlay() throws Exception {
MockAdProvider.install();
Form previous = CN.getCurrentForm();
InterstitialAd ad = new InterstitialAd("mock");
@@ -327,7 +317,8 @@ void workerShowThenDisposeDoesNotStrandQueuedDialog() throws Exception {
CountDownLatch drained = new CountDownLatch(1);
CN.callSerially(drained::countDown);
waitFor(drained, 1000);
- assertSame(previous, CN.getCurrentForm(), "Queued presentation must not resurrect a disposed ad");
+ assertSame(previous, CN.getCurrentForm(), "Queued presentation must not change the current form");
+ assertNoAd(previous);
assertEquals(Arrays.asList("shown", "impression"), events);
} finally {
ad.dispose();
@@ -344,7 +335,8 @@ void onShownCanDisposeSynchronouslyWithoutAnImpression() {
@Override public void onShown() {
events.add("shown");
assertTrue(CN.isEdt());
- assertNotSame(previous, CN.getCurrentForm());
+ assertSame(previous, CN.getCurrentForm());
+ assertEquals(1, adLayer(previous).getComponentCount());
ad.dispose();
}
@Override public void onImpression() { events.add("impression"); }
@@ -381,6 +373,7 @@ void edtDisposalCancelsAlreadyQueuedWorkerPresentation() throws Exception {
CN.callSerially(drained::countDown);
waitFor(drained, 1000);
assertSame(previous, CN.getCurrentForm());
+ assertNoAd(previous);
assertTrue(events.isEmpty(), "A disposed session must ignore pending presentation");
} finally {
ad.dispose();
From 59125bdaf039ae42162bbac0b56f0cf4ef88f560 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 12:42:51 +0300
Subject: [PATCH 10/14] Reject overlapping mock ads on the same form
---
.../codename1/ads/mock/MockAdProvider.java | 20 ++++--
.../com/codename1/ads/MockAdProviderTest.java | 64 +++++++++++++++++++
2 files changed, 78 insertions(+), 6 deletions(-)
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index 72add5ae799..7a976c404b6 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -177,12 +177,21 @@ public void show() {
cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "An ad is already showing"));
return;
}
- loaded = false;
- host = CN.getCurrentForm();
- if (host == null) {
- host = new Form();
- host.show();
+ Form target = CN.getCurrentForm();
+ if (target == null) {
+ target = new Form();
+ target.show();
}
+ Container targetLayer = target.getFormLayeredPane(MockAdProvider.class, true);
+ // Sessions share this layer. Reject overlap before consuming the ad
+ // or replacing the active session's Back command.
+ if (targetLayer.getComponentCount() != 0) {
+ cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "An ad is already showing"));
+ return;
+ }
+ loaded = false;
+ host = target;
+ layer = targetLayer;
overlay = new Container(new BorderLayout());
overlay.setGrabsPointerEvents(true);
overlay.setUIID("Form");
@@ -199,7 +208,6 @@ public void actionPerformed(ActionEvent evt) {
}
};
host.setBackCommand(closeCommand);
- layer = host.getFormLayeredPane(MockAdProvider.class, true);
layer.setLayout(new LayeredLayout());
layer.add(overlay);
host.revalidate();
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index 442890374ea..8e835624035 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -381,4 +381,68 @@ void edtDisposalCancelsAlreadyQueuedWorkerPresentation() throws Exception {
}
}
+ @FormTest
+ void overlappingAdIsRejectedAndCanRetryAfterFirstIsDisposed() {
+ MockAdProvider.install();
+ Form host = CN.getCurrentForm();
+ Command applicationBack = new Command("Application back");
+ host.setBackCommand(applicationBack);
+ InterstitialAd first = new InterstitialAd("first");
+ AppOpenAd second = new AppOpenAd("second");
+ second.setAdListener(new AdListener() {
+ @Override public void onShowFailed(AdError error) { events.add("rejected"); }
+ @Override public void onShown() { events.add("shown"); }
+ @Override public void onDismissed() { events.add("dismissed"); }
+ });
+ try {
+ first.load();
+ second.load();
+ first.show();
+ Command firstClose = host.getBackCommand();
+ second.show();
+ assertEquals(Arrays.asList("rejected"), events);
+ assertTrue(second.isLoaded(), "Rejected presentation must not consume the loaded ad");
+ assertSame(firstClose, host.getBackCommand());
+ assertEquals(1, adLayer(host).getComponentCount());
+ first.dispose();
+ assertSame(applicationBack, host.getBackCommand());
+ second.show();
+ assertEquals(Arrays.asList("rejected", "shown"), events);
+ host.getBackCommand().actionPerformed(new ActionEvent(host));
+ assertEquals(Arrays.asList("rejected", "shown", "dismissed"), events);
+ assertSame(applicationBack, host.getBackCommand());
+ assertNoAd(host);
+ } finally {
+ second.dispose();
+ first.dispose();
+ }
+ }
+
+ @FormTest
+ void disposingRejectedAdLeavesVisibleAdAndBackCommandAlone() {
+ MockAdProvider.install();
+ Form host = CN.getCurrentForm();
+ Command applicationBack = new Command("Application back");
+ host.setBackCommand(applicationBack);
+ InterstitialAd first = new InterstitialAd("first");
+ InterstitialAd second = new InterstitialAd("second");
+ try {
+ first.load();
+ second.load();
+ first.show();
+ Command firstClose = host.getBackCommand();
+ Container firstOverlay = adOverlay(host);
+ second.show();
+ second.dispose();
+ assertSame(firstClose, host.getBackCommand());
+ assertSame(firstOverlay, adOverlay(host));
+ firstClose.actionPerformed(new ActionEvent(host));
+ assertSame(applicationBack, host.getBackCommand());
+ assertNoAd(host);
+ } finally {
+ second.dispose();
+ first.dispose();
+ }
+ }
+
}
From f68bd144c590a8abed33ea562c1d8db770a600b7 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 14:45:05 +0300
Subject: [PATCH 11/14] Keep keyboard focus within mock full-screen ads
---
.../codename1/ads/mock/MockAdProvider.java | 36 ++++++++-
.../com/codename1/ads/MockAdProviderTest.java | 75 +++++++++++++++++++
2 files changed, 110 insertions(+), 1 deletion(-)
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index 7a976c404b6..6d8d9dee05d 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -42,6 +42,7 @@
import com.codename1.ui.CN;
import com.codename1.ui.Command;
import com.codename1.ui.Component;
+import com.codename1.ui.ComponentSelector;
import com.codename1.ui.Container;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
@@ -49,6 +50,7 @@
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.LayeredLayout;
+import java.util.ArrayList;
/// A deterministic, network-free ad provider for tests and screenshots. It
/// renders fixed, labelled "ads" with stable colours, text and sizes (no
@@ -126,6 +128,9 @@ private static final class MockFullScreen implements FullScreenAdSession {
private Container overlay;
private Container layer;
private Form host;
+ private Component previousFocus;
+ private final ArrayList blockedFocus = new ArrayList();
+ private Command previousDefault;
private Command previousBack;
private Command closeCommand;
@@ -197,9 +202,25 @@ public void show() {
overlay.setUIID("Form");
overlay.getAllStyles().setBgTransparency(255);
overlay.add(BorderLayout.CENTER, new Label("Mock advertisement"));
- Button close = new Button("Close ad");
+ Button close = new Button("Close ad") {
+ @Override
+ public void keyReleased(int keyCode) {
+ // Finish Form.keyReleased() before restoring the host's default
+ // command, so this same Enter press cannot activate it too.
+ CN.callSerially(() -> super.keyReleased(keyCode));
+ }
+ };
close.addActionListener(evt -> closeAd(true));
overlay.add(BorderLayout.SOUTH, close);
+ previousFocus = host.getFocused();
+ for (Component component : ComponentSelector.select("*", host)) {
+ if (component.isFocusable()) {
+ blockedFocus.add(component);
+ component.setFocusable(false);
+ }
+ }
+ previousDefault = host.getDefaultCommand();
+ host.setDefaultCommand(null);
previousBack = host.getBackCommand();
closeCommand = new Command("Close ad") {
@Override
@@ -211,6 +232,7 @@ public void actionPerformed(ActionEvent evt) {
layer.setLayout(new LayeredLayout());
layer.add(overlay);
host.revalidate();
+ close.requestFocus();
cb.onShown();
// A listener may dispose the ad synchronously from onShown().
if (overlay != null) {
@@ -230,7 +252,19 @@ private void closeAd(boolean notify) {
if (host.getBackCommand() == closeCommand) {
host.setBackCommand(previousBack);
}
+ for (Component component : blockedFocus) {
+ component.setFocusable(true);
+ }
+ blockedFocus.clear();
+ if (host.getDefaultCommand() == null) {
+ host.setDefaultCommand(previousDefault);
+ }
+ if (previousFocus == null || previousFocus.getComponentForm() == host) {
+ host.setFocused(previousFocus);
+ }
host.revalidate();
+ previousFocus = null;
+ previousDefault = null;
layer = null;
host = null;
previousBack = null;
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index 8e835624035..348fc3bbe70 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -29,6 +29,9 @@
import com.codename1.ui.Command;
import com.codename1.ui.Container;
import com.codename1.ui.Label;
+import com.codename1.ui.Component;
+import com.codename1.ui.Display;
+import com.codename1.ui.TextField;
import com.codename1.ui.CN;
import com.codename1.ui.Dialog;
import com.codename1.ui.Form;
@@ -445,4 +448,76 @@ void disposingRejectedAdLeavesVisibleAdAndBackCommandAlone() {
}
}
+ @FormTest
+ void overlayKeepsTypingAndFocusTraversalAwayFromHostControls() {
+ MockAdProvider.install();
+ Form host = CN.getCurrentForm();
+ int[] typed = {0};
+ TextField input = new TextField() {
+ @Override public void keyPressed(int key) { typed[0]++; }
+ };
+ Button underlying = new Button("Underlying action");
+ Label label = new Label("Not focusable");
+ host.addAll(input, underlying, label);
+ host.revalidate();
+ input.requestFocus();
+ assertSame(input, host.getFocused());
+ InterstitialAd ad = new InterstitialAd("mock");
+ try {
+ ad.load();
+ ad.show();
+ Component close = adOverlay(host).getComponentAt(1);
+ assertSame(close, host.getFocused());
+ host.keyPressed('a');
+ host.keyReleased('a');
+ for (int key : new int[]{Display.GAME_UP, Display.GAME_DOWN, Display.GAME_LEFT, Display.GAME_RIGHT}) {
+ host.keyPressed(key);
+ host.keyReleased(key);
+ assertSame(close, host.getFocused());
+ }
+ assertNull(host.getNextComponent(close), "Tab must not reach a covered control");
+ assertNull(host.getPreviousComponent(close), "Shift-Tab must not reach a covered control");
+ assertEquals(0, typed[0]);
+ ad.dispose();
+ assertSame(input, host.getFocused());
+ assertTrue(input.isFocusable());
+ assertTrue(underlying.isFocusable());
+ assertFalse(label.isFocusable());
+ } finally {
+ ad.dispose();
+ }
+ }
+
+ @FormTest
+ void keyboardCloseDoesNotAlsoTriggerHostDefaultCommand() {
+ MockAdProvider.install();
+ Form host = CN.getCurrentForm();
+ Button underlying = new Button("Underlying action");
+ underlying.addActionListener(evt -> events.add("underlying"));
+ host.add(underlying);
+ host.revalidate();
+ underlying.requestFocus();
+ Command defaultCommand = new Command("Default action") {
+ @Override public void actionPerformed(ActionEvent evt) { events.add("default"); }
+ };
+ host.setDefaultCommand(defaultCommand);
+ InterstitialAd ad = new InterstitialAd("mock");
+ ad.setAdListener(listener());
+ try {
+ ad.load();
+ ad.show();
+ host.keyPressed(Display.GAME_FIRE);
+ host.keyReleased(Display.GAME_FIRE);
+ CountDownLatch drained = new CountDownLatch(1);
+ CN.callSerially(drained::countDown);
+ waitFor(drained, 1000);
+ assertEquals(Arrays.asList("shown", "impression", "dismissed"), events);
+ assertNoAd(host);
+ assertSame(underlying, host.getFocused());
+ assertSame(defaultCommand, host.getDefaultCommand());
+ } finally {
+ ad.dispose();
+ }
+ }
+
}
From 2962ccf25a6f67a13e895c89daf8d969b52c03eb Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 15:33:04 +0300
Subject: [PATCH 12/14] Isolate mock full-screen ad input in its own Form
---
.../codename1/ads/mock/MockAdProvider.java | 158 ++++++--------
.../com/codename1/ads/MockAdProviderTest.java | 202 ++++++++++++------
2 files changed, 211 insertions(+), 149 deletions(-)
diff --git a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
index 6d8d9dee05d..6cff018b8bc 100644
--- a/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
+++ b/maven/cn1-ads-mock/src/main/java/com/codename1/ads/mock/MockAdProvider.java
@@ -42,15 +42,13 @@
import com.codename1.ui.CN;
import com.codename1.ui.Command;
import com.codename1.ui.Component;
-import com.codename1.ui.ComponentSelector;
import com.codename1.ui.Container;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.events.ActionEvent;
+import com.codename1.ui.events.ActionListener;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.layouts.BorderLayout;
-import com.codename1.ui.layouts.LayeredLayout;
-import java.util.ArrayList;
/// A deterministic, network-free ad provider for tests and screenshots. It
/// renders fixed, labelled "ads" with stable colours, text and sizes (no
@@ -118,21 +116,15 @@ public void loadNativeAd(String adUnitId, AdRequest request,
}
/// Deterministic full screen ad: fires the lifecycle events and presents a
- /// full-screen overlay with a Close button.
+ /// separate Form with a Close button.
private static final class MockFullScreen implements FullScreenAdSession {
private final AdFormat format;
private AdSessionCallback cb;
// Mutated on the EDT; readiness may be queried by a worker.
private volatile boolean loaded;
private boolean disposed;
- private Container overlay;
- private Container layer;
- private Form host;
- private Component previousFocus;
- private final ArrayList blockedFocus = new ArrayList();
- private Command previousDefault;
- private Command previousBack;
- private Command closeCommand;
+ private MockAdForm adForm;
+ private static MockAdForm activeForm;
MockFullScreen(AdFormat format) {
this.format = format;
@@ -178,102 +170,94 @@ public void show() {
cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "No ad loaded"));
return;
}
- if (overlay != null) {
+ if (activeForm != null) {
cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "An ad is already showing"));
return;
}
- Form target = CN.getCurrentForm();
- if (target == null) {
- target = new Form();
- target.show();
- }
- Container targetLayer = target.getFormLayeredPane(MockAdProvider.class, true);
- // Sessions share this layer. Reject overlap before consuming the ad
- // or replacing the active session's Back command.
- if (targetLayer.getComponentCount() != 0) {
- cb.onShowFailed(new AdError(AdError.CODE_INTERNAL, "mock", "An ad is already showing"));
- return;
+ Form previous = CN.getCurrentForm();
+ if (previous == null) {
+ previous = new Form();
}
loaded = false;
- host = target;
- layer = targetLayer;
- overlay = new Container(new BorderLayout());
- overlay.setGrabsPointerEvents(true);
- overlay.setUIID("Form");
- overlay.getAllStyles().setBgTransparency(255);
- overlay.add(BorderLayout.CENTER, new Label("Mock advertisement"));
- Button close = new Button("Close ad") {
- @Override
- public void keyReleased(int keyCode) {
- // Finish Form.keyReleased() before restoring the host's default
- // command, so this same Enter press cannot activate it too.
- CN.callSerially(() -> super.keyReleased(keyCode));
- }
- };
+ adForm = new MockAdForm(previous);
+ activeForm = adForm;
+ Button close = new Button("Close ad");
close.addActionListener(evt -> closeAd(true));
- overlay.add(BorderLayout.SOUTH, close);
- previousFocus = host.getFocused();
- for (Component component : ComponentSelector.select("*", host)) {
- if (component.isFocusable()) {
- blockedFocus.add(component);
- component.setFocusable(false);
- }
- }
- previousDefault = host.getDefaultCommand();
- host.setDefaultCommand(null);
- previousBack = host.getBackCommand();
- closeCommand = new Command("Close ad") {
+ adForm.add(BorderLayout.CENTER, new Label("Mock advertisement"));
+ adForm.add(BorderLayout.SOUTH, close);
+ adForm.setBackCommand(new Command("Close ad") {
@Override
public void actionPerformed(ActionEvent evt) {
closeAd(true);
}
- };
- host.setBackCommand(closeCommand);
- layer.setLayout(new LayeredLayout());
- layer.add(overlay);
- host.revalidate();
+ });
+ adForm.show();
close.requestFocus();
cb.onShown();
- // A listener may dispose the ad synchronously from onShown().
- if (overlay != null) {
+ if (adForm != null) {
cb.onImpression();
}
}
private void closeAd(boolean notify) {
- if (overlay == null) {
+ if (adForm == null) {
return;
}
- overlay.remove();
- overlay = null;
- if (layer.getComponentCount() == 0) {
- layer.remove();
- }
- if (host.getBackCommand() == closeCommand) {
- host.setBackCommand(previousBack);
- }
- for (Component component : blockedFocus) {
- component.setFocusable(true);
- }
- blockedFocus.clear();
- if (host.getDefaultCommand() == null) {
- host.setDefaultCommand(previousDefault);
- }
- if (previousFocus == null || previousFocus.getComponentForm() == host) {
- host.setFocused(previousFocus);
+ MockAdForm closing = adForm;
+ adForm = null;
+ closing.closed = true;
+ // Restore on the EDT queue so a modal caller can show again without
+ // blocking dispose(), and deliver callbacks after it becomes current.
+ CN.callSerially(() -> {
+ if (activeForm == closing) {
+ activeForm = null;
+ }
+ closing.restorePrevious(() -> {
+ if (notify) {
+ if (format == AdFormat.REWARDED || format == AdFormat.REWARDED_INTERSTITIAL) {
+ cb.onUserEarnedReward(new RewardItem("coins", 10));
+ }
+ cb.onDismissed();
+ }
+ });
+ });
+ }
+
+ private static final class MockAdForm extends Form {
+ private final Form previous;
+ private boolean closed;
+
+ MockAdForm(Form previous) {
+ super(new BorderLayout());
+ this.previous = previous;
+ // If an application dialog covered the ad when it was disposed,
+ // return to the caller when that dialog eventually uncovers it.
+ addShowListener(evt -> {
+ if (closed) {
+ CN.callSerially(() -> restorePrevious(null));
+ }
+ });
}
- host.revalidate();
- previousFocus = null;
- previousDefault = null;
- layer = null;
- host = null;
- previousBack = null;
- closeCommand = null;
- if (notify) {
- if (format == AdFormat.REWARDED || format == AdFormat.REWARDED_INTERSTITIAL) {
- cb.onUserEarnedReward(new RewardItem("coins", 10));
+
+ private void restorePrevious(Runnable afterRestore) {
+ if (CN.getCurrentForm() != this) {
+ if (afterRestore != null) {
+ afterRestore.run();
+ }
+ return;
+ }
+ if (afterRestore != null) {
+ // A modal Dialog can flush the EDT before becoming current.
+ // Its show event, rather than a queued task, marks restoration.
+ previous.addShowListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ previous.removeShowListener(this);
+ afterRestore.run();
+ }
+ });
}
- cb.onDismissed();
+ previous.showBack();
}
}
diff --git a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
index 348fc3bbe70..5de1eb94799 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ads/MockAdProviderTest.java
@@ -49,16 +49,18 @@ class MockAdProviderTest extends UITestBase {
@org.junit.jupiter.api.BeforeEach
void resetEvents() { events.clear(); }
- private Container adLayer(Form form) {
- return form.getFormLayeredPane(MockAdProvider.class, true);
+ private Container adContent(Form form) {
+ return form.getContentPane();
}
- private Container adOverlay(Form form) {
- return (Container) adLayer(form).getComponentAt(0);
+ private void drainEdt() {
+ CountDownLatch drained = new CountDownLatch(1);
+ CN.callSerially(() -> CN.callSerially(drained::countDown));
+ waitFor(drained, 1000);
}
private void assertNoAd(Form form) {
- assertEquals(0, adLayer(form).getComponentCount());
+ assertSame(form, CN.getCurrentForm());
}
private AdListener listener() {
@@ -70,7 +72,7 @@ private AdListener listener() {
}
@FormTest
- void interstitialOverlayStaysVisibleUntilClosed() {
+ void interstitialFormStaysVisibleUntilClosed() {
Form previous = CN.getCurrentForm();
Command previousBack = new Command("Application back");
previous.setBackCommand(previousBack);
@@ -80,22 +82,26 @@ void interstitialOverlayStaysVisibleUntilClosed() {
ad.load();
ad.show();
Form showing = CN.getCurrentForm();
- assertSame(previous, showing);
- assertEquals(showing.getWidth(), adOverlay(showing).getWidth());
- assertEquals(showing.getHeight(), adOverlay(showing).getHeight());
- assertEquals("Mock advertisement", ((Label) adOverlay(showing).getComponentAt(0)).getText());
+ assertNotSame(previous, showing);
+ assertFalse(showing instanceof Dialog);
+ assertEquals(implementation.getDisplayWidth(), showing.getWidth());
+ assertEquals(implementation.getDisplayHeight(), showing.getHeight());
+ assertEquals("Mock advertisement", ((Label) adContent(showing).getComponentAt(0)).getText());
assertEquals(Arrays.asList("shown", "impression"), events);
Command closeCommand = showing.getBackCommand();
- Button close = (Button) adOverlay(showing).getComponentAt(1);
+ Button close = (Button) adContent(showing).getComponentAt(1);
close.pressed();
close.released();
+ drainEdt();
assertSame(previous, CN.getCurrentForm());
assertEquals(Arrays.asList("shown", "impression", "dismissed"), events);
closeCommand.actionPerformed(new ActionEvent(showing));
- assertNoAd(showing);
- assertSame(previousBack, showing.getBackCommand());
+ drainEdt();
+ assertNoAd(previous);
+ assertSame(previousBack, previous.getBackCommand());
assertEquals(3, events.size(), "Closing twice must not repeat callbacks");
ad.dispose();
+ drainEdt();
}
@FormTest
@@ -110,9 +116,11 @@ void rewardedBackCloseDeliversRewardBeforeDismissal() {
Form showing = CN.getCurrentForm();
assertEquals(Arrays.asList("shown", "impression"), events);
showing.getBackCommand().actionPerformed(new ActionEvent(showing));
+ drainEdt();
assertSame(previous, CN.getCurrentForm());
assertEquals(Arrays.asList("shown", "impression", "reward", "dismissed"), events);
ad.dispose();
+ drainEdt();
}
@FormTest
@@ -125,12 +133,13 @@ void disposingVisibleAdRestoresFormWithoutRewardOrDismissal() {
ad.load();
ad.show();
ad.dispose();
+ drainEdt();
assertSame(previous, CN.getCurrentForm());
assertEquals(Arrays.asList("shown", "impression"), events);
}
@FormTest
- void overlayLeavesModalCallerAvailableToDismissalCallback() {
+ void adRestoresModalCallerBeforeDismissalCallback() {
MockAdProvider.install();
Dialog dialog = new Dialog("Modal caller");
Form destination = new Form("After dismissal");
@@ -142,21 +151,31 @@ void overlayLeavesModalCallerAvailableToDismissalCallback() {
assertSame(dialog, CN.getCurrentForm());
assertNoAd(dialog);
dialog.dispose();
+ drainEdt();
destination.show();
}
});
ad.setOnUserEarnedRewardListener(reward -> events.add("reward"));
- dialog.addShowListener(evt -> CN.callSerially(() -> {
- try {
- ad.load();
- ad.show();
- dialog.getBackCommand().actionPerformed(new ActionEvent(dialog));
- } catch (Throwable t) {
- failure.set(t);
- } finally {
- dialog.dispose();
+ boolean[] started = {false};
+ dialog.addShowListener(evt -> {
+ if (started[0]) {
+ return;
}
- }));
+ started[0] = true;
+ CN.callSerially(() -> {
+ try {
+ ad.load();
+ ad.show();
+ CN.getCurrentForm().getBackCommand().actionPerformed(new ActionEvent(dialog));
+ drainEdt();
+ } catch (Throwable t) {
+ failure.set(t);
+ } finally {
+ dialog.dispose();
+ drainEdt();
+ }
+ });
+ });
try {
dialog.show();
assertNull(failure.get());
@@ -164,6 +183,7 @@ void overlayLeavesModalCallerAvailableToDismissalCallback() {
assertSame(destination, CN.getCurrentForm());
} finally {
ad.dispose();
+ drainEdt();
}
}
@@ -179,22 +199,25 @@ void appOpenAdWithoutPreviousFormCanCloseGoBackOrDispose() {
ad.load();
ad.show();
Form showing = CN.getCurrentForm();
- assertEquals("Mock advertisement", ((Label) adOverlay(showing).getComponentAt(0)).getText());
+ assertEquals("Mock advertisement", ((Label) adContent(showing).getComponentAt(0)).getText());
if (action == 0) {
- Button close = (Button) adOverlay(showing).getComponentAt(1);
+ Button close = (Button) adContent(showing).getComponentAt(1);
close.pressed();
close.released();
+ drainEdt();
} else if (action == 1) {
showing.getBackCommand().actionPerformed(new ActionEvent(showing));
+ drainEdt();
} else {
ad.dispose();
+ drainEdt();
}
assertNotNull(CN.getCurrentForm());
- assertSame(showing, CN.getCurrentForm());
- assertNoAd(showing);
+ assertNotSame(showing, CN.getCurrentForm());
assertEquals(action == 2 ? Arrays.asList("shown", "impression")
: Arrays.asList("shown", "impression", "dismissed"), events);
ad.dispose();
+ drainEdt();
}
}
@@ -208,18 +231,23 @@ void modelessCallerStaysModelessAfterAdDisposal() {
ad.load();
ad.show();
ad.dispose();
+ drainEdt();
assertSame(caller, CN.getCurrentForm());
caller.dispose();
+ drainEdt();
boolean[] disposedDuringShow = {false};
caller.addShowListener(evt -> CN.callSerially(() -> {
disposedDuringShow[0] = true;
caller.dispose();
+ drainEdt();
}));
caller.show();
assertFalse(disposedDuringShow[0], "A reused modeless caller must not wait for disposal");
} finally {
ad.dispose();
+ drainEdt();
caller.dispose();
+ drainEdt();
}
}
@@ -232,11 +260,12 @@ void disposalDoesNotUndoApplicationNavigation() {
Form destination = new Form("Application destination");
destination.show();
ad.dispose();
+ drainEdt();
assertSame(destination, CN.getCurrentForm());
}
@FormTest
- void disposalRemovesOverlayWithoutChangingNestedDialogs() {
+ void disposedAdReturnsToCallerAfterNestedDialogsClose() {
MockAdProvider.install();
Form application = CN.getCurrentForm();
for (int depth = 1; depth <= 2; depth++) {
@@ -252,12 +281,15 @@ void disposalRemovesOverlayWithoutChangingNestedDialogs() {
overlays[i].showModeless();
}
ad.dispose();
+ drainEdt();
ad.dispose();
+ drainEdt();
assertSame(overlays[depth - 1], CN.getCurrentForm(),
"Disposing an underlying ad must leave the top dialog visible");
assertEquals(Arrays.asList("shown", "impression"), events);
for (int i = depth - 1; i >= 0; i--) {
overlays[i].dispose();
+ drainEdt();
assertSame(i == 0 ? application : overlays[i - 1], CN.getCurrentForm(),
"Dialog disposal must restore its original caller");
}
@@ -265,9 +297,11 @@ void disposalRemovesOverlayWithoutChangingNestedDialogs() {
for (int i = depth - 1; i >= 0; i--) {
if (overlays[i] != null) {
overlays[i].dispose();
+ drainEdt();
}
}
ad.dispose();
+ drainEdt();
application.show();
}
}
@@ -285,17 +319,21 @@ void disposalDoesNotUndoNavigationToAnUnrelatedDialog() {
destination.show();
unrelated.showModeless();
ad.dispose();
+ drainEdt();
assertSame(unrelated, CN.getCurrentForm());
unrelated.dispose();
+ drainEdt();
assertSame(destination, CN.getCurrentForm());
} finally {
ad.dispose();
+ drainEdt();
unrelated.dispose();
+ drainEdt();
}
}
@FormTest
- void workerShowThenDisposeRemovesQueuedOverlay() throws Exception {
+ void workerShowThenDisposeRestoresCaller() throws Exception {
MockAdProvider.install();
Form previous = CN.getCurrentForm();
InterstitialAd ad = new InterstitialAd("mock");
@@ -317,14 +355,13 @@ void workerShowThenDisposeRemovesQueuedOverlay() throws Exception {
worker.join(1000);
assertFalse(worker.isAlive(), "Worker operations must not wait for the EDT");
assertNull(failure.get());
- CountDownLatch drained = new CountDownLatch(1);
- CN.callSerially(drained::countDown);
- waitFor(drained, 1000);
- assertSame(previous, CN.getCurrentForm(), "Queued presentation must not change the current form");
+ drainEdt();
+ assertSame(previous, CN.getCurrentForm(), "Queued disposal must restore the original form");
assertNoAd(previous);
assertEquals(Arrays.asList("shown", "impression"), events);
} finally {
ad.dispose();
+ drainEdt();
previous.show();
}
}
@@ -338,14 +375,15 @@ void onShownCanDisposeSynchronouslyWithoutAnImpression() {
@Override public void onShown() {
events.add("shown");
assertTrue(CN.isEdt());
- assertSame(previous, CN.getCurrentForm());
- assertEquals(1, adLayer(previous).getComponentCount());
+ assertNotSame(previous, CN.getCurrentForm());
+ assertEquals(2, adContent(CN.getCurrentForm()).getComponentCount());
ad.dispose();
}
@Override public void onImpression() { events.add("impression"); }
});
ad.load();
ad.show();
+ drainEdt();
assertSame(previous, CN.getCurrentForm());
assertEquals(Arrays.asList("shown"), events);
}
@@ -372,14 +410,13 @@ void edtDisposalCancelsAlreadyQueuedWorkerPresentation() throws Exception {
assertNull(failure.get());
// An EDT caller can dispose before the queued load/show get a turn.
ad.dispose();
- CountDownLatch drained = new CountDownLatch(1);
- CN.callSerially(drained::countDown);
- waitFor(drained, 1000);
+ drainEdt();
assertSame(previous, CN.getCurrentForm());
assertNoAd(previous);
assertTrue(events.isEmpty(), "A disposed session must ignore pending presentation");
} finally {
ad.dispose();
+ drainEdt();
previous.show();
}
}
@@ -401,23 +438,28 @@ void overlappingAdIsRejectedAndCanRetryAfterFirstIsDisposed() {
first.load();
second.load();
first.show();
- Command firstClose = host.getBackCommand();
+ Form showing = CN.getCurrentForm();
+ Command firstClose = showing.getBackCommand();
second.show();
assertEquals(Arrays.asList("rejected"), events);
assertTrue(second.isLoaded(), "Rejected presentation must not consume the loaded ad");
- assertSame(firstClose, host.getBackCommand());
- assertEquals(1, adLayer(host).getComponentCount());
+ assertSame(firstClose, showing.getBackCommand());
+ assertSame(showing, CN.getCurrentForm());
first.dispose();
+ drainEdt();
assertSame(applicationBack, host.getBackCommand());
second.show();
assertEquals(Arrays.asList("rejected", "shown"), events);
- host.getBackCommand().actionPerformed(new ActionEvent(host));
+ CN.getCurrentForm().getBackCommand().actionPerformed(new ActionEvent(host));
+ drainEdt();
assertEquals(Arrays.asList("rejected", "shown", "dismissed"), events);
assertSame(applicationBack, host.getBackCommand());
assertNoAd(host);
} finally {
second.dispose();
+ drainEdt();
first.dispose();
+ drainEdt();
}
}
@@ -433,23 +475,28 @@ void disposingRejectedAdLeavesVisibleAdAndBackCommandAlone() {
first.load();
second.load();
first.show();
- Command firstClose = host.getBackCommand();
- Container firstOverlay = adOverlay(host);
+ Form showing = CN.getCurrentForm();
+ Command firstClose = showing.getBackCommand();
+ Container firstContent = adContent(CN.getCurrentForm());
second.show();
second.dispose();
- assertSame(firstClose, host.getBackCommand());
- assertSame(firstOverlay, adOverlay(host));
+ drainEdt();
+ assertSame(firstClose, showing.getBackCommand());
+ assertSame(firstContent, adContent(CN.getCurrentForm()));
firstClose.actionPerformed(new ActionEvent(host));
+ drainEdt();
assertSame(applicationBack, host.getBackCommand());
assertNoAd(host);
} finally {
second.dispose();
+ drainEdt();
first.dispose();
+ drainEdt();
}
}
@FormTest
- void overlayKeepsTypingAndFocusTraversalAwayFromHostControls() {
+ void adFormKeepsTypingAndFocusTraversalAwayFromHostControls() {
MockAdProvider.install();
Form host = CN.getCurrentForm();
int[] typed = {0};
@@ -466,25 +513,28 @@ void overlayKeepsTypingAndFocusTraversalAwayFromHostControls() {
try {
ad.load();
ad.show();
- Component close = adOverlay(host).getComponentAt(1);
- assertSame(close, host.getFocused());
- host.keyPressed('a');
- host.keyReleased('a');
+ Form showing = CN.getCurrentForm();
+ Component close = adContent(showing).getComponentAt(1);
+ assertSame(close, showing.getFocused());
+ showing.keyPressed('a');
+ showing.keyReleased('a');
for (int key : new int[]{Display.GAME_UP, Display.GAME_DOWN, Display.GAME_LEFT, Display.GAME_RIGHT}) {
- host.keyPressed(key);
- host.keyReleased(key);
- assertSame(close, host.getFocused());
+ showing.keyPressed(key);
+ showing.keyReleased(key);
+ assertSame(close, showing.getFocused());
}
- assertNull(host.getNextComponent(close), "Tab must not reach a covered control");
- assertNull(host.getPreviousComponent(close), "Shift-Tab must not reach a covered control");
+ assertNull(showing.getNextComponent(close), "Tab must not reach a covered control");
+ assertNull(showing.getPreviousComponent(close), "Shift-Tab must not reach a covered control");
assertEquals(0, typed[0]);
ad.dispose();
+ drainEdt();
assertSame(input, host.getFocused());
assertTrue(input.isFocusable());
assertTrue(underlying.isFocusable());
assertFalse(label.isFocusable());
} finally {
ad.dispose();
+ drainEdt();
}
}
@@ -506,17 +556,45 @@ void keyboardCloseDoesNotAlsoTriggerHostDefaultCommand() {
try {
ad.load();
ad.show();
- host.keyPressed(Display.GAME_FIRE);
- host.keyReleased(Display.GAME_FIRE);
- CountDownLatch drained = new CountDownLatch(1);
- CN.callSerially(drained::countDown);
- waitFor(drained, 1000);
+ Form showing = CN.getCurrentForm();
+ showing.keyPressed(Display.GAME_FIRE);
+ showing.keyReleased(Display.GAME_FIRE);
+ drainEdt();
assertEquals(Arrays.asList("shown", "impression", "dismissed"), events);
assertNoAd(host);
assertSame(underlying, host.getFocused());
assertSame(defaultCommand, host.getDefaultCommand());
} finally {
ad.dispose();
+ drainEdt();
+ }
+ }
+
+ @FormTest
+ void fullScreenAdBlocksHostKeyAndGameKeyListeners() {
+ MockAdProvider.install();
+ Form host = CN.getCurrentForm();
+ host.addKeyListener('x', evt -> events.add("shortcut"));
+ host.addKeyListener(Display.GAME_FIRE, evt -> events.add("enter"));
+ host.addGameKeyListener(Display.GAME_FIRE, evt -> events.add("game-fire"));
+ InterstitialAd ad = new InterstitialAd("mock");
+ try {
+ ad.load();
+ ad.show();
+ Form showing = CN.getCurrentForm();
+ showing.keyPressed('x');
+ showing.keyReleased('x');
+ showing.keyPressed(Display.GAME_FIRE);
+ showing.keyReleased(Display.GAME_FIRE);
+ drainEdt();
+ assertTrue(events.isEmpty(), "Ad input must not invoke covered form shortcuts: " + events);
+ assertSame(host, CN.getCurrentForm());
+ host.keyReleased('x');
+ host.keyReleased(Display.GAME_FIRE);
+ assertEquals(Arrays.asList("shortcut", "enter", "game-fire"), events);
+ } finally {
+ ad.dispose();
+ drainEdt();
}
}
From af07e57f6fdb4ae42990509f777f509f202c046b Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 16:34:53 +0300
Subject: [PATCH 13/14] Scope Swift compatibility search paths to AdMob apps
---
.../workflows/ad-cn1lib-ios-native-check.yml | 2 +
.../builders/AdMobIosLinkSettings.java | 62 +++++++++++++++++
.../com/codename1/builders/IPhoneBuilder.java | 1 +
.../builders/AdMobIosLinkSettingsTest.java | 69 +++++++++++++++++++
scripts/check-admob-ios-link.sh | 33 ++++++++-
.../template.xcodeproj/project.pbxproj | 8 ---
6 files changed, 166 insertions(+), 9 deletions(-)
create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java
create mode 100644 maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AdMobIosLinkSettingsTest.java
diff --git a/.github/workflows/ad-cn1lib-ios-native-check.yml b/.github/workflows/ad-cn1lib-ios-native-check.yml
index d1d282330dc..2c423052792 100644
--- a/.github/workflows/ad-cn1lib-ios-native-check.yml
+++ b/.github/workflows/ad-cn1lib-ios-native-check.yml
@@ -21,6 +21,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'scripts/check-admob-ios-link.sh'
+ - 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java'
- 'vm/ByteCodeTranslator/**'
- 'vm/pom.xml'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
@@ -31,6 +32,7 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'scripts/check-admob-ios-link.sh'
+ - 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java'
- 'vm/ByteCodeTranslator/**'
- 'vm/pom.xml'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java
new file mode 100644
index 00000000000..928f00b0e51
--- /dev/null
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java
@@ -0,0 +1,62 @@
+/*
+ * 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 java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+/** Link settings needed by the Google Mobile Ads pod's prebuilt Swift objects. */
+final class AdMobIosLinkSettings {
+ private AdMobIosLinkSettings() {
+ }
+
+ static void apply(File project, String pods) throws IOException {
+ boolean usesAdMob = false;
+ if (pods != null) {
+ for (String pod : pods.split("[,;]")) {
+ if ("Google-Mobile-Ads-SDK".equals(pod.trim().split("\\s+", 2)[0])) {
+ usesAdMob = true;
+ break;
+ }
+ }
+ }
+ if (!usesAdMob) {
+ return;
+ }
+ String contents = new String(Files.readAllBytes(project.toPath()), StandardCharsets.UTF_8);
+ // TOOLCHAIN_DIR can select the separately installed Metal toolchain.
+ String swiftPath = "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)";
+ if (contents.contains(swiftPath)) {
+ return;
+ }
+ String anchor = "LIBRARY_SEARCH_PATHS = (";
+ if (!contents.contains(anchor)) {
+ throw new IOException("Cannot add AdMob link paths: generated project has no library search paths");
+ }
+ contents = contents.replace(anchor, anchor + "\n\t\t\t\t\t\"" + swiftPath
+ + "\",\n\t\t\t\t\t\"$(SDKROOT)/usr/lib/swift\",");
+ Files.write(project.toPath(), contents.getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
index af84a03d582..0b316e96f0d 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
@@ -6677,6 +6677,7 @@ public void usesClassMethod(String cls, String method) {
// that the change will work for all builds. I made it "only" for the cocoapods version
// to prevent inadvertent breaking of versioned builds etc...
if (runPods) {
+ AdMobIosLinkSettings.apply(pbx, iosPods);
replaceAllInFile(pbx, "ARCHS = [^;]+;", "ARCHS = \"\\$(ARCHS_STANDARD)\";");
replaceAllInFile(pbx, "VALID_ARCHS = [^;]+;", "VALID_ARCHS = \"\\$(ARCHS_STANDARD)\";");
} else {
diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AdMobIosLinkSettingsTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AdMobIosLinkSettingsTest.java
new file mode 100644
index 00000000000..927be88995e
--- /dev/null
+++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AdMobIosLinkSettingsTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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 java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import static org.junit.jupiter.api.Assertions.*;
+
+class AdMobIosLinkSettingsTest {
+ @TempDir Path temp;
+
+ private byte[] template() throws Exception {
+ return Files.readAllBytes(Paths.get("../../vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj"));
+ }
+
+ @Test
+ void ordinaryAndUnrelatedPodProjectsRemainByteForByteUnchanged() throws Exception {
+ byte[] original = template();
+ assertFalse(new String(original, StandardCharsets.UTF_8).contains("/usr/lib/swift"));
+ Path project = temp.resolve("project.pbxproj");
+ for (String pods : new String[]{null, "", "Alamofire", "Google-Mobile-Ads-SDK-Other ~> 13.0"}) {
+ Files.write(project, original);
+ AdMobIosLinkSettings.apply(project.toFile(), pods);
+ assertArrayEquals(original, Files.readAllBytes(project), String.valueOf(pods));
+ }
+ }
+
+ @Test
+ void adMobGetsPathsInBothConfigurationsAndPreservesExistingPaths() throws Exception {
+ Path project = temp.resolve("project.pbxproj");
+ for (String pods : new String[]{"Google-Mobile-Ads-SDK ~> 13.0",
+ "OtherPod, Google-Mobile-Ads-SDK ~> 13.0; ThirdPod", "Google-Mobile-Ads-SDK"}) {
+ Files.write(project, template());
+ AdMobIosLinkSettings.apply(project.toFile(), pods);
+ String result = new String(Files.readAllBytes(project), StandardCharsets.UTF_8);
+ assertEquals(2, result.split("XcodeDefault.xctoolchain/usr/lib/swift", -1).length - 1);
+ assertEquals(2, result.split("/usr/lib/swift\",", -1).length - 1);
+ assertTrue(result.contains("$(PROJECT_DIR)/template-src"));
+ assertTrue(result.contains("$(inherited)"));
+ byte[] first = Files.readAllBytes(project);
+ AdMobIosLinkSettings.apply(project.toFile(), pods);
+ assertArrayEquals(first, Files.readAllBytes(project));
+ }
+ }
+}
diff --git a/scripts/check-admob-ios-link.sh b/scripts/check-admob-ios-link.sh
index 4f31758eaec..cc909e0bf4a 100755
--- a/scripts/check-admob-ios-link.sh
+++ b/scripts/check-admob-ios-link.sh
@@ -45,6 +45,37 @@ java -jar "$ROOT/vm/ByteCodeTranslator/dist/ByteCodeTranslator.jar" ios \
"$PROBE/Sources" "$PROBE/generated" AdMobLinkProbe com.codenameone.test \
AdMobLinkProbe 1.0 ios "${LIBS:-none}"
PROJECT="$PROBE/generated/dist"
+# Apply exactly the AdMob-only app settings used by IPhoneBuilder. The shared
+# translator template must remain free of Swift paths for unrelated apps.
+cat > "$PROBE/ApplyAdMobSettings.java" <<'JAVA'
+package com.codename1.builders;
+import java.io.*;
+import java.nio.file.*;
+import java.util.*;
+public class ApplyAdMobSettings {
+ public static void main(String[] args) throws Exception {
+ File project = new File(args[0]);
+ byte[] original = Files.readAllBytes(project.toPath());
+ if (new String(original, "UTF-8").contains("/usr/lib/swift")) {
+ throw new AssertionError("Swift paths leaked into the shared translator template");
+ }
+ AdMobIosLinkSettings.apply(project, "UnrelatedPod");
+ if (!Arrays.equals(original, Files.readAllBytes(project.toPath()))) {
+ throw new AssertionError("AdMob settings changed an unrelated project");
+ }
+ Properties hints = new Properties();
+ try (InputStream input = new FileInputStream(args[1])) { hints.load(input); }
+ AdMobIosLinkSettings.apply(project, hints.getProperty("codename1.arg.ios.pods"));
+ }
+}
+JAVA
+mkdir -p "$PROBE/link-settings"
+javac -d "$PROBE/link-settings" \
+ "$ROOT/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java" \
+ "$PROBE/ApplyAdMobSettings.java"
+java -cp "$PROBE/link-settings" com.codename1.builders.ApplyAdMobSettings \
+ "$PROJECT/AdMobLinkProbe.xcodeproj/project.pbxproj" \
+ "$ROOT/maven/cn1-admob/common/codenameone_library_required.properties"
cp "$PROBE"/{cn1_globals.h,cn1_virtual_thread.h,cn1_class_method_index.h,Prefix.pch} "$PROJECT/"
plutil -convert json -o "$PROBE/project.json" "$PROJECT/AdMobLinkProbe.xcodeproj/project.pbxproj"
python3 - "$ROOT" "$PROBE" "$PROJECT" "$LIBS" <<'PYTHON'
@@ -74,7 +105,7 @@ for lib in filter(None, sys.argv[4].split(';')):
assert obj.get('lastKnownFileType') == 'sourcecode.text-based-dylib-definition', obj
assert ref in linked and ref not in copied, 'Library hint is not a linker input: ' + lib
# Only replace translated runtime sources with the callback stubs above. Keep
-# the translator's Frameworks phase, SDK paths and LIBRARY_SEARCH_PATHS verbatim.
+# the translator's Frameworks phase and the builder's AdMob-only search paths.
source_names = {'main.m', 'com_codename1_ads_admob_AdMobNativeImpl.m'}
for obj in objects.values():
if obj['isa'] == 'PBXSourcesBuildPhase':
diff --git a/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj b/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj
index 37c451c9b06..304a953222f 100644
--- a/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj
+++ b/vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj
@@ -309,10 +309,6 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/template-src",
- // Prebuilt pods can require Swift compatibility archives even in
- // an Objective-C app. TOOLCHAIN_DIR can point at the Metal toolchain.
- "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)",
- "$(SDKROOT)/usr/lib/swift",
);
};
name = Debug;
@@ -356,10 +352,6 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/template-src",
- // Prebuilt pods can require Swift compatibility archives even in
- // an Objective-C app. TOOLCHAIN_DIR can point at the Metal toolchain.
- "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)",
- "$(SDKROOT)/usr/lib/swift",
);
};
name = Release;
From dcce3bcaae374df45652ba303f0e8baf761a2f36 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 10 Sep 2026 16:46:48 +0300
Subject: [PATCH 14/14] Use existing Swift support for the AdMob library
---
.../workflows/ad-cn1lib-ios-native-check.yml | 2 -
.../objectivec/CN1AdMobSwiftSupport.swift | 26 +++++++
.../builders/AdMobIosLinkSettings.java | 62 -----------------
.../com/codename1/builders/IPhoneBuilder.java | 1 -
.../builders/AdMobIosLinkSettingsTest.java | 69 -------------------
scripts/check-admob-ios-link.sh | 59 ++++++++--------
6 files changed, 53 insertions(+), 166 deletions(-)
create mode 100644 maven/cn1-admob/ios/src/main/objectivec/CN1AdMobSwiftSupport.swift
delete mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java
delete mode 100644 maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AdMobIosLinkSettingsTest.java
diff --git a/.github/workflows/ad-cn1lib-ios-native-check.yml b/.github/workflows/ad-cn1lib-ios-native-check.yml
index 2c423052792..d1d282330dc 100644
--- a/.github/workflows/ad-cn1lib-ios-native-check.yml
+++ b/.github/workflows/ad-cn1lib-ios-native-check.yml
@@ -21,7 +21,6 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'scripts/check-admob-ios-link.sh'
- - 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java'
- 'vm/ByteCodeTranslator/**'
- 'vm/pom.xml'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
@@ -32,7 +31,6 @@ on:
- 'maven/cn1-applovin/**'
- 'maven/cn1-unity-levelplay/**'
- 'scripts/check-admob-ios-link.sh'
- - 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java'
- 'vm/ByteCodeTranslator/**'
- 'vm/pom.xml'
- '.github/workflows/ad-cn1lib-ios-native-check.yml'
diff --git a/maven/cn1-admob/ios/src/main/objectivec/CN1AdMobSwiftSupport.swift b/maven/cn1-admob/ios/src/main/objectivec/CN1AdMobSwiftSupport.swift
new file mode 100644
index 00000000000..261bb3206ff
--- /dev/null
+++ b/maven/cn1-admob/ios/src/main/objectivec/CN1AdMobSwiftSupport.swift
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2012, 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.
+ */
+// Google Mobile Ads includes prebuilt Swift objects. An app-target Swift source
+// activates CN1's existing Swift support and Xcode's compatibility-library
+// linkage, including when all of the application's own native code is Objective-C.
+import Foundation
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java
deleted file mode 100644
index 928f00b0e51..00000000000
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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 java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-
-/** Link settings needed by the Google Mobile Ads pod's prebuilt Swift objects. */
-final class AdMobIosLinkSettings {
- private AdMobIosLinkSettings() {
- }
-
- static void apply(File project, String pods) throws IOException {
- boolean usesAdMob = false;
- if (pods != null) {
- for (String pod : pods.split("[,;]")) {
- if ("Google-Mobile-Ads-SDK".equals(pod.trim().split("\\s+", 2)[0])) {
- usesAdMob = true;
- break;
- }
- }
- }
- if (!usesAdMob) {
- return;
- }
- String contents = new String(Files.readAllBytes(project.toPath()), StandardCharsets.UTF_8);
- // TOOLCHAIN_DIR can select the separately installed Metal toolchain.
- String swiftPath = "$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)";
- if (contents.contains(swiftPath)) {
- return;
- }
- String anchor = "LIBRARY_SEARCH_PATHS = (";
- if (!contents.contains(anchor)) {
- throw new IOException("Cannot add AdMob link paths: generated project has no library search paths");
- }
- contents = contents.replace(anchor, anchor + "\n\t\t\t\t\t\"" + swiftPath
- + "\",\n\t\t\t\t\t\"$(SDKROOT)/usr/lib/swift\",");
- Files.write(project.toPath(), contents.getBytes(StandardCharsets.UTF_8));
- }
-}
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
index 0b316e96f0d..af84a03d582 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
@@ -6677,7 +6677,6 @@ public void usesClassMethod(String cls, String method) {
// that the change will work for all builds. I made it "only" for the cocoapods version
// to prevent inadvertent breaking of versioned builds etc...
if (runPods) {
- AdMobIosLinkSettings.apply(pbx, iosPods);
replaceAllInFile(pbx, "ARCHS = [^;]+;", "ARCHS = \"\\$(ARCHS_STANDARD)\";");
replaceAllInFile(pbx, "VALID_ARCHS = [^;]+;", "VALID_ARCHS = \"\\$(ARCHS_STANDARD)\";");
} else {
diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AdMobIosLinkSettingsTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AdMobIosLinkSettingsTest.java
deleted file mode 100644
index 927be88995e..00000000000
--- a/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/AdMobIosLinkSettingsTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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 java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-import static org.junit.jupiter.api.Assertions.*;
-
-class AdMobIosLinkSettingsTest {
- @TempDir Path temp;
-
- private byte[] template() throws Exception {
- return Files.readAllBytes(Paths.get("../../vm/ByteCodeTranslator/src/template/template.xcodeproj/project.pbxproj"));
- }
-
- @Test
- void ordinaryAndUnrelatedPodProjectsRemainByteForByteUnchanged() throws Exception {
- byte[] original = template();
- assertFalse(new String(original, StandardCharsets.UTF_8).contains("/usr/lib/swift"));
- Path project = temp.resolve("project.pbxproj");
- for (String pods : new String[]{null, "", "Alamofire", "Google-Mobile-Ads-SDK-Other ~> 13.0"}) {
- Files.write(project, original);
- AdMobIosLinkSettings.apply(project.toFile(), pods);
- assertArrayEquals(original, Files.readAllBytes(project), String.valueOf(pods));
- }
- }
-
- @Test
- void adMobGetsPathsInBothConfigurationsAndPreservesExistingPaths() throws Exception {
- Path project = temp.resolve("project.pbxproj");
- for (String pods : new String[]{"Google-Mobile-Ads-SDK ~> 13.0",
- "OtherPod, Google-Mobile-Ads-SDK ~> 13.0; ThirdPod", "Google-Mobile-Ads-SDK"}) {
- Files.write(project, template());
- AdMobIosLinkSettings.apply(project.toFile(), pods);
- String result = new String(Files.readAllBytes(project), StandardCharsets.UTF_8);
- assertEquals(2, result.split("XcodeDefault.xctoolchain/usr/lib/swift", -1).length - 1);
- assertEquals(2, result.split("/usr/lib/swift\",", -1).length - 1);
- assertTrue(result.contains("$(PROJECT_DIR)/template-src"));
- assertTrue(result.contains("$(inherited)"));
- byte[] first = Files.readAllBytes(project);
- AdMobIosLinkSettings.apply(project.toFile(), pods);
- assertArrayEquals(first, Files.readAllBytes(project));
- }
- }
-}
diff --git a/scripts/check-admob-ios-link.sh b/scripts/check-admob-ios-link.sh
index cc909e0bf4a..ea30b76efd6 100755
--- a/scripts/check-admob-ios-link.sh
+++ b/scripts/check-admob-ios-link.sh
@@ -45,37 +45,6 @@ java -jar "$ROOT/vm/ByteCodeTranslator/dist/ByteCodeTranslator.jar" ios \
"$PROBE/Sources" "$PROBE/generated" AdMobLinkProbe com.codenameone.test \
AdMobLinkProbe 1.0 ios "${LIBS:-none}"
PROJECT="$PROBE/generated/dist"
-# Apply exactly the AdMob-only app settings used by IPhoneBuilder. The shared
-# translator template must remain free of Swift paths for unrelated apps.
-cat > "$PROBE/ApplyAdMobSettings.java" <<'JAVA'
-package com.codename1.builders;
-import java.io.*;
-import java.nio.file.*;
-import java.util.*;
-public class ApplyAdMobSettings {
- public static void main(String[] args) throws Exception {
- File project = new File(args[0]);
- byte[] original = Files.readAllBytes(project.toPath());
- if (new String(original, "UTF-8").contains("/usr/lib/swift")) {
- throw new AssertionError("Swift paths leaked into the shared translator template");
- }
- AdMobIosLinkSettings.apply(project, "UnrelatedPod");
- if (!Arrays.equals(original, Files.readAllBytes(project.toPath()))) {
- throw new AssertionError("AdMob settings changed an unrelated project");
- }
- Properties hints = new Properties();
- try (InputStream input = new FileInputStream(args[1])) { hints.load(input); }
- AdMobIosLinkSettings.apply(project, hints.getProperty("codename1.arg.ios.pods"));
- }
-}
-JAVA
-mkdir -p "$PROBE/link-settings"
-javac -d "$PROBE/link-settings" \
- "$ROOT/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AdMobIosLinkSettings.java" \
- "$PROBE/ApplyAdMobSettings.java"
-java -cp "$PROBE/link-settings" com.codename1.builders.ApplyAdMobSettings \
- "$PROJECT/AdMobLinkProbe.xcodeproj/project.pbxproj" \
- "$ROOT/maven/cn1-admob/common/codenameone_library_required.properties"
cp "$PROBE"/{cn1_globals.h,cn1_virtual_thread.h,cn1_class_method_index.h,Prefix.pch} "$PROJECT/"
plutil -convert json -o "$PROBE/project.json" "$PROJECT/AdMobLinkProbe.xcodeproj/project.pbxproj"
python3 - "$ROOT" "$PROBE" "$PROJECT" "$LIBS" <<'PYTHON'
@@ -105,7 +74,7 @@ for lib in filter(None, sys.argv[4].split(';')):
assert obj.get('lastKnownFileType') == 'sourcecode.text-based-dylib-definition', obj
assert ref in linked and ref not in copied, 'Library hint is not a linker input: ' + lib
# Only replace translated runtime sources with the callback stubs above. Keep
-# the translator's Frameworks phase and the builder's AdMob-only search paths.
+# the translator's Frameworks phase, SDK paths and library search paths.
source_names = {'main.m', 'com_codename1_ads_admob_AdMobNativeImpl.m'}
for obj in objects.values():
if obj['isa'] == 'PBXSourcesBuildPhase':
@@ -129,6 +98,32 @@ with (project / 'AdMobLinkProbe.xcodeproj/project.pbxproj').open('wb') as output
plistlib.dump(data, output)
PYTHON
(cd "$PROJECT" && pod install)
+# Mirror IPhoneBuilder's existing Swift-source handling after pod integration.
+# Do not supply custom Swift library search paths to make the probe link.
+ruby - "$PROJECT" <<'RUBY'
+require 'xcodeproj'
+root = ARGV[0]
+project = Xcodeproj::Project.open(File.join(root, 'AdMobLinkProbe.xcodeproj'))
+app = project.targets.find { |target| target.name == 'AdMobLinkProbe' }
+Dir.glob(File.join(root, 'AdMobLinkProbe-src', '**', '*.swift')).each do |path|
+ relative = Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
+ ref = project.files.find { |file| file.path == relative } || project.main_group.new_file(relative)
+ app.source_build_phase.add_file_reference(ref, true) unless app.source_build_phase.files_references.include?(ref)
+ app.resources_build_phase.remove_file_reference(ref)
+end
+unless Dir.glob(File.join(root, '**', '*.swift')).empty?
+ File.write(File.join(root, 'cn1-Bridging-Header.h'), "// Codename One generated Swift bridging header\n")
+ project.build_configurations.each { |config| config.build_settings['SWIFT_VERSION'] = '5.0' }
+ app.build_configurations.each do |config|
+ config.build_settings['DEFINES_MODULE'] = 'YES'
+ config.build_settings['SWIFT_OBJC_BRIDGING_HEADER'] = '$(SRCROOT)/cn1-Bridging-Header.h'
+ end
+end
+support = app.source_build_phase.files_references.select { |file| File.basename(file.path) == 'CN1AdMobSwiftSupport.swift' }
+raise 'The shipped AdMob Swift source is missing from the app compile phase' unless support.size == 1
+raise 'AdMob Swift source must not be copied as a resource' if app.resources_build_phase.files_references.include?(support.first)
+project.save
+RUBY
xcodebuild -workspace "$PROJECT/AdMobLinkProbe.xcworkspace" -scheme AdMobLinkProbe \
-configuration Release -sdk "$SDK" -derivedDataPath "$PROBE/build-$SDK" \
CODE_SIGNING_ALLOWED=NO build