Skip to content

Commit 1b432e3

Browse files
committed
Improve Robolectric screenshot capture
1 parent 1a32f06 commit 1b432e3

File tree

1 file changed

+179
-19
lines changed

1 file changed

+179
-19
lines changed

scripts/templates/HelloCodenameOneUiTest.java.tmpl

Lines changed: 179 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import android.graphics.Bitmap;
77
import android.graphics.Canvas;
88
import android.os.SystemClock;
99
import android.util.DisplayMetrics;
10+
import android.view.TextureView;
1011
import android.view.View;
12+
import android.view.ViewGroup;
1113

14+
import com.codename1.impl.android.AndroidImplementation;
1215
import com.codename1.ui.Display;
1316
import com.codename1.ui.Form;
1417
import com.codename1.ui.Image;
@@ -17,8 +20,11 @@ import com.codename1.ui.util.ImageIO;
1720
import java.io.File;
1821
import java.io.FileOutputStream;
1922
import java.io.IOException;
23+
import java.lang.reflect.Field;
2024
import 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

2329
import org.junit.After;
2430
import 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

Comments
 (0)