@@ -7,8 +7,11 @@ import android.graphics.Bitmap;
77import android.graphics.Canvas;
88import android.os.SystemClock;
99import android.util.DisplayMetrics;
10+ import android.view.TextureView;
1011import android.view.View;
12+ import android.view.ViewGroup;
1113
14+ import com.codename1.impl.android.AndroidImplementation;
1215import com.codename1.ui.Display;
1316import com.codename1.ui.Form;
1417import com.codename1.ui.Image;
@@ -17,8 +20,11 @@ import com.codename1.ui.util.ImageIO;
1720import java.io.File;
1821import java.io.FileOutputStream;
1922import java.io.IOException;
23+ import java.lang.reflect.Field;
2024import java.util.concurrent.Callable;
21- import java.util.concurrent.FutureTask;
25+ import java.util.concurrent.CountDownLatch;
26+ import java.util.concurrent.TimeUnit;
27+ import java.util.concurrent.atomic.AtomicReference;
2228
2329import org.junit.After;
2430import org.junit.Before;
@@ -64,10 +70,7 @@ public class @MAIN_NAME@UiTest {
6470 View decorView = activity.getWindow().getDecorView();
6571 assertNotNull("Activity decor view should be available", decorView);
6672
67- ensureViewHasLayout(decorView);
68-
69- ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
70- Bitmap androidScreenshot = captureBitmap(decorView);
73+ Bitmap androidScreenshot = captureAndroidScreenshot(decorView);
7174 assertTrue("Android screenshot width should be positive", androidScreenshot.getWidth() > 0);
7275 assertTrue("Android screenshot height should be positive", androidScreenshot.getHeight() > 0);
7376 assertTrue(
@@ -78,7 +81,6 @@ public class @MAIN_NAME@UiTest {
7881 assertTrue("Android screenshot file should exist", androidScreenshotFile.isFile());
7982 assertTrue("Android screenshot file should not be empty", androidScreenshotFile.length() > 0L);
8083
81- ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
8284 Image codenameOneScreenshot = captureCodenameOneScreenshot();
8385 assertNotNull("Codename One screenshot should be available", codenameOneScreenshot);
8486 assertTrue("Codename One screenshot width should be positive", codenameOneScreenshot.getWidth() > 0);
@@ -94,6 +96,21 @@ public class @MAIN_NAME@UiTest {
9496 assertTrue("Codename One screenshot file should not be empty", codenameOneScreenshotFile.length() > 0L);
9597 }
9698
99+ private Bitmap captureAndroidScreenshot(View decorView) throws Exception {
100+ ensureViewHasLayout(decorView);
101+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
102+ flushCodenameOneGraphics();
103+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
104+
105+ View codenameOneSurface = findCodenameOneSurface(decorView);
106+ Bitmap surfaceBitmap = captureSurfaceBitmap(codenameOneSurface);
107+ if (surfaceBitmap != null && hasRenderableContent(surfaceBitmap)) {
108+ return surfaceBitmap;
109+ }
110+
111+ return captureBitmap(decorView);
112+ }
113+
97114 private static Bitmap captureBitmap(View view) {
98115 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
99116 Canvas canvas = new Canvas(bitmap);
@@ -130,7 +147,7 @@ public class @MAIN_NAME@UiTest {
130147 private static boolean hasRenderableContent(Image screenshot) throws Exception {
131148 int width = screenshot.getWidth();
132149 int height = screenshot.getHeight();
133- int[] pixels = callOnEdt (screenshot::getRGB);
150+ int[] pixels = callSeriallyAndWait (screenshot::getRGB);
134151 return hasRenderableContent(pixels, width, height);
135152 }
136153
@@ -169,11 +186,11 @@ public class @MAIN_NAME@UiTest {
169186 private static File saveCodenameOneScreenshot(Image screenshot, String fileName) throws Exception {
170187 File outputDir = resolveArtifactDirectory();
171188 File screenshotFile = new File(outputDir, fileName);
172- ImageIO io = callOnEdt (() -> Display.getInstance().getImageIO());
189+ ImageIO io = callSeriallyAndWait (() -> Display.getInstance().getImageIO());
173190 assertNotNull("Codename One ImageIO should be available", io);
174191 try (FileOutputStream out = new FileOutputStream(screenshotFile)) {
175192 FileOutputStream stream = out;
176- callOnEdt (() -> {
193+ callSeriallyAndWait (() -> {
177194 io.save(screenshot, stream, ImageIO.FORMAT_PNG, 1.0f);
178195 return null;
179196 });
@@ -208,20 +225,163 @@ public class @MAIN_NAME@UiTest {
208225 }
209226
210227 private static Image captureCodenameOneScreenshot() throws Exception {
211- return callOnEdt (() -> Display.getInstance().captureScreen());
228+ return callSeriallyAndWait (() -> Display.getInstance().captureScreen());
212229 }
213230
214- private static <T> T callOnEdt(Callable<T> callable) throws Exception {
215- FutureTask<T> task = new FutureTask<>(callable);
216- Display.getInstance().callSerially(task);
217- long deadline = SystemClock.uptimeMillis() + EDT_TIMEOUT_MS;
218- while (!task.isDone() && SystemClock.uptimeMillis() < deadline) {
219- ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
220- SystemClock.sleep(4L);
231+ private static Bitmap captureSurfaceBitmap(View view) {
232+ if (view == null) {
233+ return null;
234+ }
235+ if (view instanceof TextureView) {
236+ Bitmap bitmap = ((TextureView) view).getBitmap();
237+ if (bitmap != null) {
238+ return bitmap;
239+ }
240+ }
241+
242+ Bitmap reflectionBitmap = extractCodenameOneViewBitmap(view);
243+ if (reflectionBitmap != null) {
244+ return reflectionBitmap;
245+ }
246+
247+ if (view instanceof ViewGroup) {
248+ ViewGroup group = (ViewGroup) view;
249+ for (int i = 0; i < group.getChildCount(); i++) {
250+ Bitmap childBitmap = captureSurfaceBitmap(group.getChildAt(i));
251+ if (childBitmap != null) {
252+ return childBitmap;
253+ }
254+ }
255+ }
256+ return null;
257+ }
258+
259+ private static Bitmap extractCodenameOneViewBitmap(View view) {
260+ try {
261+ Field cn1Field = view.getClass().getDeclaredField("cn1View");
262+ cn1Field.setAccessible(true);
263+ Object cn1View = cn1Field.get(view);
264+ if (cn1View != null) {
265+ Field bitmapField = cn1View.getClass().getDeclaredField("bitmap");
266+ bitmapField.setAccessible(true);
267+ Object bitmapValue = bitmapField.get(cn1View);
268+ if (bitmapValue instanceof Bitmap) {
269+ Bitmap bitmap = (Bitmap) bitmapValue;
270+ if (bitmap.getWidth() > 0 && bitmap.getHeight() > 0) {
271+ Bitmap copy = bitmap.copy(Bitmap.Config.ARGB_8888, false);
272+ if (copy == null) {
273+ copy = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
274+ Canvas canvas = new Canvas(copy);
275+ canvas.drawBitmap(bitmap, 0f, 0f, null);
276+ }
277+ return copy;
278+ }
279+ }
280+ }
281+ } catch (NoSuchFieldException ignored) {
282+ // View does not expose a Codename One backing field.
283+ } catch (IllegalAccessException e) {
284+ throw new RuntimeException("Unable to access Codename One view bitmap", e);
285+ }
286+ return null;
287+ }
288+
289+ private static View findCodenameOneSurface(View root) {
290+ if (root == null) {
291+ return null;
292+ }
293+ if (root.getId() == 2001) {
294+ return root;
295+ }
296+ if (root.getClass().getName().contains("CodenameOne")) {
297+ return root;
298+ }
299+ if (root instanceof ViewGroup) {
300+ ViewGroup group = (ViewGroup) root;
301+ for (int i = 0; i < group.getChildCount(); i++) {
302+ View child = group.getChildAt(i);
303+ View match = findCodenameOneSurface(child);
304+ if (match != null) {
305+ return match;
306+ }
307+ }
221308 }
222- if (!task.isDone()) {
309+ return null;
310+ }
311+
312+ private static void flushCodenameOneGraphics() throws Exception {
313+ callSeriallyAndWait(() -> {
314+ Form current = Display.getInstance().getCurrent();
315+ if (current != null) {
316+ current.revalidate();
317+ current.repaint();
318+ }
319+ return null;
320+ });
321+ callSeriallyAndWait(() -> {
322+ AndroidImplementation impl = getAndroidImplementation();
323+ if (impl != null) {
324+ impl.flushGraphics();
325+ }
326+ return null;
327+ });
328+ }
329+
330+ private static AndroidImplementation getAndroidImplementation() {
331+ try {
332+ Field implField = Display.class.getDeclaredField("impl");
333+ implField.setAccessible(true);
334+ Object impl = implField.get(null);
335+ if (impl instanceof AndroidImplementation) {
336+ return (AndroidImplementation) impl;
337+ }
338+ return null;
339+ } catch (NoSuchFieldException | IllegalAccessException e) {
340+ throw new RuntimeException("Unable to access Codename One implementation", e);
341+ }
342+ }
343+
344+ private static <T> T callSeriallyAndWait(Callable<T> callable) throws Exception {
345+ CountDownLatch latch = new CountDownLatch(1);
346+ AtomicReference<T> result = new AtomicReference<>();
347+ AtomicReference<Throwable> error = new AtomicReference<>();
348+ Display.getInstance().callSerially(() -> {
349+ try {
350+ result.set(callable.call());
351+ } catch (Throwable t) {
352+ error.set(t);
353+ } finally {
354+ latch.countDown();
355+ }
356+ });
357+ boolean completed;
358+ try {
359+ completed = awaitLatch(latch, EDT_TIMEOUT_MS);
360+ } catch (InterruptedException e) {
361+ Thread.currentThread().interrupt();
362+ throw e;
363+ }
364+ if (!completed) {
223365 throw new AssertionError("Timed out waiting for Codename One EDT task to finish");
224366 }
225- return task.get();
367+ if (error.get() != null) {
368+ Throwable throwable = error.get();
369+ if (throwable instanceof Exception) {
370+ throw (Exception) throwable;
371+ }
372+ throw new RuntimeException(throwable);
373+ }
374+ return result.get();
375+ }
376+
377+ private static boolean awaitLatch(CountDownLatch latch, long timeoutMs) throws InterruptedException {
378+ long deadline = SystemClock.uptimeMillis() + timeoutMs;
379+ while (SystemClock.uptimeMillis() < deadline) {
380+ if (latch.await(4L, TimeUnit.MILLISECONDS)) {
381+ return true;
382+ }
383+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
384+ }
385+ return latch.await(0L, TimeUnit.MILLISECONDS);
226386 }
227387}
0 commit comments