@@ -201,6 +201,7 @@ xmlstarlet sel -N "$NS" -t -c "/mvn:project/mvn:build/mvn:plugins" -n "$ROOT_POM
201201[ -f " $APP_DIR /build.sh" ] && chmod +x " $APP_DIR /build.sh"
202202
203203SETTINGS_FILE=" $APP_DIR /common/codenameone_settings.properties"
204+ echo " codename1.arg.android.useAndroidX=true" >> " $SETTINGS_FILE "
204205[ -f " $SETTINGS_FILE " ] || { ba_log " codenameone_settings.properties not found at $SETTINGS_FILE " >&2 ; exit 1; }
205206
206207# --- Read settings ---
@@ -265,6 +266,246 @@ if [ -z "$GRADLE_PROJECT_DIR" ]; then
265266 exit 1
266267fi
267268
269+ ba_log " Configuring instrumentation test sources in $GRADLE_PROJECT_DIR "
270+
271+ # Ensure AndroidX flags in gradle.properties
272+ # --- BEGIN: robust Gradle patch for AndroidX tests ---
273+ GRADLE_PROPS=" $GRADLE_PROJECT_DIR /gradle.properties"
274+ grep -q ' ^android.useAndroidX=' " $GRADLE_PROPS " 2> /dev/null || echo ' android.useAndroidX=true' >> " $GRADLE_PROPS "
275+ grep -q ' ^android.enableJetifier=' " $GRADLE_PROPS " 2> /dev/null || echo ' android.enableJetifier=true' >> " $GRADLE_PROPS "
276+
277+ APP_BUILD_GRADLE=" $GRADLE_PROJECT_DIR /app/build.gradle"
278+ ROOT_BUILD_GRADLE=" $GRADLE_PROJECT_DIR /build.gradle"
279+
280+ # Ensure repos in both root and app
281+ for F in " $ROOT_BUILD_GRADLE " " $APP_BUILD_GRADLE " ; do
282+ if [ -f " $F " ]; then
283+ if ! grep -qE ' ^\s*repositories\s*{' " $F " ; then
284+ cat >> " $F " << 'EOS '
285+
286+ repositories {
287+ google()
288+ mavenCentral()
289+ }
290+ EOS
291+ else
292+ grep -q ' google()' " $F " || sed -E -i ' 0,/repositories[[:space:]]*\{/s//repositories {\n google()\n mavenCentral()/' " $F "
293+ grep -q ' mavenCentral()' " $F " || sed -E -i ' 0,/repositories[[:space:]]*\{/s//repositories {\n google()\n mavenCentral()/' " $F "
294+ fi
295+ fi
296+ done
297+
298+ # Edit app/build.gradle
299+ python3 - " $APP_BUILD_GRADLE " << 'PY '
300+ import sys, re, pathlib
301+ p = pathlib.Path(sys.argv[1]); txt = p.read_text(); orig = txt; changed = False
302+
303+ def strip_block(name, s):
304+ return re.sub(rf'(?ms)^\s*{name}\s*\{{.*?\}}\s*', '', s)
305+
306+ module_view = strip_block('buildscript', strip_block('pluginManagement', txt))
307+
308+ # 1) android { compileSdkVersion/targetSdkVersion }
309+ def ensure_sdk(body):
310+ # If android { ... } exists, update/insert inside defaultConfig and the android block
311+ if re.search(r'(?m)^\s*android\s*\{', body):
312+ # compileSdkVersion
313+ if re.search(r'(?m)^\s*compileSdkVersion\s+\d+', body) is None:
314+ body = re.sub(r'(?m)(^\s*android\s*\{)', r'\1\n compileSdkVersion 33', body, count=1)
315+ else:
316+ body = re.sub(r'(?m)^\s*compileSdkVersion\s+\d+', ' compileSdkVersion 33', body)
317+ # targetSdkVersion
318+ if re.search(r'(?ms)^\s*defaultConfig\s*\{.*?^\s*\}', body):
319+ dc = re.search(r'(?ms)^\s*defaultConfig\s*\{.*?^\s*\}', body)
320+ block = dc.group(0)
321+ if re.search(r'(?m)^\s*targetSdkVersion\s+\d+', block):
322+ block2 = re.sub(r'(?m)^\s*targetSdkVersion\s+\d+', ' targetSdkVersion 33', block)
323+ else:
324+ block2 = re.sub(r'(\{\s*)', r'\1\n targetSdkVersion 33', block, count=1)
325+ body = body[:dc.start()] + block2 + body[dc.end():]
326+ else:
327+ body = re.sub(r'(?m)(^\s*android\s*\{)', r'\1\n defaultConfig {\n targetSdkVersion 33\n }', body, count=1)
328+ else:
329+ # No android block at all: add minimal
330+ body += '\n\nandroid {\n compileSdkVersion 33\n defaultConfig { targetSdkVersion 33 }\n}\n'
331+ return body
332+
333+ txt2 = ensure_sdk(txt)
334+ if txt2 != txt: txt = txt2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True
335+
336+ # 2) testInstrumentationRunner -> AndroidX
337+ if "androidx.test.runner.AndroidJUnitRunner" not in module_view:
338+ t2, n = re.subn(r'(?m)^\s*testInstrumentationRunner\s*".*?"\s*$', ' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"', txt)
339+ if n == 0:
340+ t2, n = re.subn(r'(?m)(^\s*defaultConfig\s*\{)', r'\1\n testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"', txt, count=1)
341+ if n == 0:
342+ t2, n = re.subn(r'(?ms)(^\s*android\s*\{)', r'\1\n defaultConfig {\n testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"\n }', txt, count=1)
343+ if n: txt = t2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True
344+
345+ # 3) remove legacy useLibrary lines
346+ t2, n = re.subn(r'(?m)^\s*useLibrary\s+\'android\.test\.(base|mock|runner)\'\s*$', '', txt)
347+ if n: txt = t2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True
348+
349+ # 4) deps: choose androidTestImplementation vs androidTestCompile
350+ uses_modern = re.search(r'(?m)^\s*(implementation|api|testImplementation|androidTestImplementation)\b', module_view) is not None
351+ conf = "androidTestImplementation" if uses_modern else "androidTestCompile"
352+ need = [
353+ ("androidx.test.ext:junit:1.1.5", conf), # AndroidJUnit4
354+ ("androidx.test:runner:1.5.2", conf),
355+ ("androidx.test:core:1.5.0", conf),
356+ ("androidx.test.services:storage:1.4.2", conf),
357+ ]
358+ to_add = [(c, k) for (c, k) in need if c not in module_view]
359+
360+ if to_add:
361+ block = "\n\ndependencies {\n" + "".join([f" {k} \"{c}\"\n" for c, k in to_add]) + "}\n"
362+ txt = txt.rstrip() + block
363+ changed = True
364+
365+ if changed and txt != orig:
366+ if not txt.endswith("\n"): txt += "\n"
367+ p.write_text(txt)
368+ print(f"Patched app/build.gradle (SDK=33; deps via {conf})")
369+ else:
370+ print("No changes needed in app/build.gradle")
371+ PY
372+ # --- END: robust Gradle patch ---
373+
374+ echo " ----- app/build.gradle tail -----"
375+ tail -n 80 " $APP_BUILD_GRADLE " | sed ' s/^/| /'
376+ echo " ---------------------------------"
377+
378+ TEST_SRC_DIR=" $GRADLE_PROJECT_DIR /app/src/androidTest/java/${PACKAGE_PATH} "
379+ mkdir -p " $TEST_SRC_DIR "
380+ TEST_CLASS=" $TEST_SRC_DIR /HelloCodenameOneInstrumentedTest.java"
381+ cat > " $TEST_CLASS " << 'EOF '
382+ package @PACKAGE@;
383+
384+ import android.app.Activity;
385+ import android.content.Context;
386+ import android.content.Intent;
387+ import android.graphics.Bitmap;
388+ import android.graphics.Canvas;
389+ import android.util.Base64;
390+ import android.util.DisplayMetrics;
391+ import android.view.View;
392+
393+ import androidx.test.core.app.ActivityScenario;
394+ import androidx.test.core.app.ApplicationProvider;
395+ import androidx.test.ext.junit.runners.AndroidJUnit4;
396+
397+ import org.junit.Assert;
398+ import org.junit.Test;
399+ import org.junit.runner.RunWith;
400+
401+ import java.io.ByteArrayOutputStream;
402+
403+ @RunWith(AndroidJUnit4.class)
404+ public class HelloCodenameOneInstrumentedTest {
405+
406+ private static void println(String s) { System.out.println(s); }
407+
408+ @Test
409+ public void testUseAppContext_andEmitScreenshot() throws Exception {
410+ Context ctx = ApplicationProvider.getApplicationContext();
411+ String pkg = "@PACKAGE@";
412+ Assert.assertEquals("Package mismatch", pkg, ctx.getPackageName());
413+
414+ // Resolve real launcher intent (don’t hard-code activity)
415+ Intent launch = ctx.getPackageManager().getLaunchIntentForPackage(pkg);
416+ if (launch == null) {
417+ // Fallback MAIN/LAUNCHER inside this package
418+ Intent q = new Intent(Intent.ACTION_MAIN);
419+ q.addCategory(Intent.CATEGORY_LAUNCHER);
420+ q.setPackage(pkg);
421+ launch = q;
422+ }
423+ launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
424+
425+ println("CN1SS:INFO: about to launch Activity");
426+ byte[] pngBytes = null;
427+
428+ try (ActivityScenario<Activity> scenario = ActivityScenario.launch(launch)) {
429+ // give the activity a tiny moment to layout
430+ Thread.sleep(750);
431+
432+ println("CN1SS:INFO: activity launched");
433+
434+ final byte[][] holder = new byte[1][];
435+ scenario.onActivity(activity -> {
436+ try {
437+ View root = activity.getWindow().getDecorView().getRootView();
438+ int w = root.getWidth();
439+ int h = root.getHeight();
440+ if (w <= 0 || h <= 0) {
441+ DisplayMetrics dm = activity.getResources().getDisplayMetrics();
442+ w = Math.max(1, dm.widthPixels);
443+ h = Math.max(1, dm.heightPixels);
444+ int sw = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY);
445+ int sh = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY);
446+ root.measure(sw, sh);
447+ root.layout(0, 0, w, h);
448+ println("CN1SS:INFO: forced layout to " + w + "x" + h);
449+ } else {
450+ println("CN1SS:INFO: natural layout " + w + "x" + h);
451+ }
452+
453+ Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
454+ Canvas c = new Canvas(bmp);
455+ root.draw(c);
456+
457+ ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.max(1024, w * h / 2));
458+ boolean ok = bmp.compress(Bitmap.CompressFormat.PNG, 100, baos);
459+ if (!ok) throw new RuntimeException("Bitmap.compress returned false");
460+ holder[0] = baos.toByteArray();
461+ println("CN1SS:INFO: png_bytes=" + holder[0].length);
462+ } catch (Throwable t) {
463+ println("CN1SS:ERR: onActivity " + t);
464+ t.printStackTrace(System.out);
465+ }
466+ });
467+
468+ pngBytes = holder[0];
469+ } catch (Throwable t) {
470+ println("CN1SS:ERR: launch " + t);
471+ t.printStackTrace(System.out);
472+ }
473+
474+ if (pngBytes == null || pngBytes.length == 0) {
475+ println("CN1SS:END"); // terminator for the runner parser
476+ Assert.fail("Screenshot capture produced 0 bytes");
477+ return;
478+ }
479+
480+ // Chunk & emit (safe for Gradle/logcat capture)
481+ String b64 = Base64.encodeToString(pngBytes, Base64.NO_WRAP);
482+ final int CHUNK = 2000;
483+ int count = 0;
484+ for (int pos = 0; pos < b64.length(); pos += CHUNK) {
485+ int end = Math.min(pos + CHUNK, b64.length());
486+ System.out.println("CN1SS:" + String.format("%06d", pos) + ":" + b64.substring(pos, end));
487+ count++;
488+ }
489+ println("CN1SS:INFO: chunks=" + count + " total_b64_len=" + b64.length());
490+ System.out.println("CN1SS:END");
491+ System.out.flush();
492+ }
493+ }
494+ EOF
495+ sed -i " s|@PACKAGE@|$PACKAGE_NAME |g" " $TEST_CLASS "
496+ ba_log " Created instrumentation test at $TEST_CLASS "
497+
498+ DEFAULT_ANDROID_TEST=" $GRADLE_PROJECT_DIR /app/src/androidTest/java/com/example/myapplication2/ExampleInstrumentedTest.java"
499+ if [ -f " $DEFAULT_ANDROID_TEST " ]; then
500+ rm -f " $DEFAULT_ANDROID_TEST "
501+ ba_log " Removed default instrumentation stub at $DEFAULT_ANDROID_TEST "
502+ DEFAULT_ANDROID_TEST_DIR=" $( dirname " $DEFAULT_ANDROID_TEST " ) "
503+ DEFAULT_ANDROID_TEST_PARENT=" $( dirname " $DEFAULT_ANDROID_TEST_DIR " ) "
504+ rmdir " $DEFAULT_ANDROID_TEST_DIR " 2> /dev/null || true
505+ rmdir " $DEFAULT_ANDROID_TEST_PARENT " 2> /dev/null || true
506+ rmdir " $( dirname " $DEFAULT_ANDROID_TEST_PARENT " ) " 2> /dev/null || true
507+ fi
508+
268509ba_log " Invoking Gradle build in $GRADLE_PROJECT_DIR "
269510chmod +x " $GRADLE_PROJECT_DIR /gradlew"
270511ORIGINAL_JAVA_HOME=" $JAVA_HOME "
@@ -282,4 +523,13 @@ export JAVA_HOME="$ORIGINAL_JAVA_HOME"
282523
283524APK_PATH=$( find " $GRADLE_PROJECT_DIR " -path " */outputs/apk/debug/*.apk" | head -n 1 || true)
284525[ -n " $APK_PATH " ] || { ba_log " Gradle build completed but no APK was found" >&2 ; exit 1; }
285- ba_log " Successfully built Android APK at $APK_PATH "
526+ ba_log " Successfully built Android APK at $APK_PATH "
527+
528+ if [ -n " ${GITHUB_OUTPUT:- } " ]; then
529+ {
530+ echo " gradle_project_dir=$GRADLE_PROJECT_DIR "
531+ echo " apk_path=$APK_PATH "
532+ echo " instrumentation_test_class=$PACKAGE_NAME .HelloCodenameOneInstrumentedTest"
533+ } >> " $GITHUB_OUTPUT "
534+ ba_log " Published GitHub Actions outputs for downstream steps"
535+ fi
0 commit comments