Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/purchase-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,43 @@ jobs:
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
- name: Preinstall Android emulator package
run: |
set -euo pipefail
ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
if [ -z "$ANDROID_SDK_ROOT" ]; then
if [ -d "/usr/local/lib/android/sdk" ]; then
ANDROID_SDK_ROOT="/usr/local/lib/android/sdk"
elif [ -d "$HOME/Android/Sdk" ]; then
ANDROID_SDK_ROOT="$HOME/Android/Sdk"
fi
fi
if [ -z "$ANDROID_SDK_ROOT" ] || [ ! -d "$ANDROID_SDK_ROOT" ]; then
echo "::error::Android SDK root not found"
exit 1
fi
export ANDROID_SDK_ROOT ANDROID_HOME="$ANDROID_SDK_ROOT"

SDKMANAGER_BIN="$(command -v sdkmanager || true)"
if [ -z "$SDKMANAGER_BIN" ] && [ -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then
SDKMANAGER_BIN="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
fi
if [ -z "$SDKMANAGER_BIN" ]; then
echo "::error::sdkmanager not found under $ANDROID_SDK_ROOT"
exit 1
fi

printf 'y\n%.0s' {1..100} | "$SDKMANAGER_BIN" --licenses >/dev/null || true
for attempt in 1 2 3; do
if "$SDKMANAGER_BIN" --install emulator --channel=0; then
exit 0
fi
echo "Android emulator package install failed on attempt ${attempt}; retrying"
rm -rf "$ANDROID_SDK_ROOT/.temp" || true
sleep 10
done
echo "::error::Unable to install Android emulator package after retries"
exit 1
# The emulator boot on GitHub-hosted runners intermittently dies before the
# device ever appears ("device 'emulator-5554' not found" -> the runner
# action's own boot timeout). One explicit retry recovers the infra lottery;
Expand Down
3 changes: 3 additions & 0 deletions Ports/iOSPort/nativeSources/CN1RenderingView.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
- (void)keyboardDoneClicked;
- (void)keyboardNextClicked;
- (void)textFieldDidChange;
@optional
- (void)invalidateRetainedFramebuffer;
- (void)prepareRetainedFramebufferForDrawRect:(CGRect)rect displayWidth:(int)displayWidth displayHeight:(int)displayHeight;
@end

#endif
7 changes: 7 additions & 0 deletions Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#endif

extern BOOL isIOS10();
extern void repaintUI();
int pendingRemoteNotificationRegistrations = 0;

BOOL isAppSuspended = NO;
Expand Down Expand Up @@ -290,6 +291,12 @@ - (void)cn1ApplicationWillEnterForeground
com_codename1_impl_ios_IOSImplementation_applicationWillEnterForeground__(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG);
CodenameOne_GLViewController* vc = [CodenameOne_GLViewController instance];
if (vc != nil) {
#ifdef CN1_USE_METAL
id renderingView = [vc eaglView];
if ([renderingView respondsToSelector:@selector(invalidateRetainedFramebuffer)]) {
[renderingView invalidateRetainedFramebuffer];
}
#endif
// Defer to the next runloop so UIKit can settle the view bounds
// after the snapshot rotation. updateCanvas itself also
// orientation-validates the bounds for an extra safety net under
Expand Down
19 changes: 17 additions & 2 deletions Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -4299,7 +4299,22 @@ - (void)drawFrame:(CGRect)rect allowInactive:(BOOL)allowInactive
return;
}
#endif
[[self eaglView] setFramebuffer];
id renderingView = [self eaglView];
#ifdef CN1_USE_METAL
BOOL hasScreenOps = NO;
if (currentTarget != nil) {
for (ExecutableOp *ex in currentTarget) {
if ([ex target] == nil) {
hasScreenOps = YES;
break;
}
}
}
if (hasScreenOps && [renderingView respondsToSelector:@selector(prepareRetainedFramebufferForDrawRect:displayWidth:displayHeight:)]) {
[renderingView prepareRetainedFramebufferForDrawRect:rect displayWidth:displayWidth displayHeight:displayHeight];
}
#endif
[renderingView setFramebuffer];
GLErrorLog;
BOOL drewContentOps = NO;
if(currentTarget != nil) {
Expand Down Expand Up @@ -4446,7 +4461,7 @@ - (void)drawFrame:(CGRect)rect allowInactive:(BOOL)allowInactive
}
GLErrorLog;

[[self eaglView] presentFramebuffer];
[renderingView presentFramebuffer];
GLErrorLog;
if (drewContentOps && !cn1LaunchPlaceholderDone) {
// First frame with real content is on screen -- fade out the launch
Expand Down
11 changes: 10 additions & 1 deletion Ports/iOSPort/nativeSources/METALView.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
// turn or by the next normal presentFramebuffer -- whichever lands first.
// Read/written on the main thread only, so no synchronisation is needed.
BOOL needsResizePresent;

// The persistent screenTexture is private GPU storage. iOS may discard or
// leave it stale across app suspension, and CN1 can resume with only
// partial dirty-region flushes. Mark it invalid on foreground; the first
// full-screen repaint clears it before drawing fresh content.
BOOL retainedFramebufferInvalid;
BOOL clearRetainedFramebufferOnNextFrame;
}
@property (nonatomic, retain) id<MTLCommandQueue> commandQueue;
@property (nonatomic, retain) id<MTLCommandBuffer> commandBuffer;
Expand Down Expand Up @@ -82,9 +89,11 @@
- (BOOL)presentFramebuffer;
-(void)presentPreservedFrameIfNeeded;
-(void)updateFrameBufferSize:(int)w h:(int)h;
-(void)invalidateRetainedFramebuffer;
-(void)prepareRetainedFramebufferForDrawRect:(CGRect)rect displayWidth:(int)displayWidth displayHeight:(int)displayHeight;
-(void)textFieldDidChange;
-(void) keyboardDoneClicked;
-(void) keyboardNextClicked;
-(void) addPeerComponent:(UIView*) view;
@end
#endif
#endif
40 changes: 38 additions & 2 deletions Ports/iOSPort/nativeSources/METALView.m
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,36 @@ -(void)presentPreservedFrameIfNeeded {
[presentCb commit];
}

-(void)invalidateRetainedFramebuffer {
retainedFramebufferInvalid = YES;
clearRetainedFramebufferOnNextFrame = NO;
// A preserved resize frame is also stale after a suspend/resume cycle.
needsResizePresent = NO;
}

-(void)prepareRetainedFramebufferForDrawRect:(CGRect)rect displayWidth:(int)displayWidth displayHeight:(int)displayHeight {
if (!retainedFramebufferInvalid) {
return;
}
// During rotation, displayWidth/displayHeight can briefly lag the Metal
// drawable. Only consume the invalidation when the repaint covers the
// actual retained framebuffer we are about to load from.
int targetWidth = framebufferWidth > 0 ? framebufferWidth : displayWidth;
int targetHeight = framebufferHeight > 0 ? framebufferHeight : displayHeight;
if (targetWidth <= 0 || targetHeight <= 0) {
return;
}
CGFloat minX = rect.origin.x;
CGFloat minY = rect.origin.y;
CGFloat maxX = rect.origin.x + rect.size.width;
CGFloat maxY = rect.origin.y + rect.size.height;
if (minX <= 0.0f && minY <= 0.0f &&
maxX >= (CGFloat)targetWidth && maxY >= (CGFloat)targetHeight) {
clearRetainedFramebufferOnNextFrame = YES;
retainedFramebufferInvalid = NO;
}
}

-(void)createRenderPassDescriptor {
if (self.screenTexture == nil) {
self.renderPassDescriptor = nil;
Expand All @@ -532,7 +562,13 @@ -(void)createRenderPassDescriptor {
// which would wipe everything each frame) — CN1 only queues diff ops
// per frame; the OpenGL path relies on its renderbuffer persisting.
colorAttachment.texture = self.screenTexture;
colorAttachment.loadAction = MTLLoadActionLoad;
if (clearRetainedFramebufferOnNextFrame) {
colorAttachment.loadAction = MTLLoadActionClear;
colorAttachment.clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0);
clearRetainedFramebufferOnNextFrame = NO;
} else {
colorAttachment.loadAction = MTLLoadActionLoad;
}
colorAttachment.storeAction = MTLStoreActionStore;
// Attach the Stencil8 texture for polygon-shape clipping (#3921).
// Cleared at the start of every frame and discarded at the end --
Expand Down Expand Up @@ -775,4 +811,4 @@ -(void)layoutSubviews


@end
#endif
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ private static int testTimeoutMs(BaseTest testClass) {
&& testClass instanceof LightweightPickerButtonsScreenshotTest) {
return 45000;
}
if (!"HTML5".equals(Display.getInstance().getPlatformName())
&& testClass instanceof ToastBarTopPositionScreenshotTest) {
return 45000;
}
return "HTML5".equals(Display.getInstance().getPlatformName())
? TEST_TIMEOUT_MS_HTML5
: TEST_TIMEOUT_MS_NATIVE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.codename1.ui.Display;
import com.codename1.ui.FontImage;
import com.codename1.ui.Form;
import com.codename1.ui.Image;
import com.codename1.ui.Label;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
Expand Down Expand Up @@ -49,9 +50,10 @@ protected void registerReadyCallback(Form parent, Runnable run) {
// the form WITHOUT the toast, a run-to-run "toast present vs absent" flake that no
// golden can reconcile (max_channel_delta 191). Rather than guess a timer, poll until
// the toast component is actually laid out + visible on the current form, re-issuing
// the show if it never took, then hand off to the base settle+capture. Bounded so a
// genuine show failure degrades to a capture instead of hanging the suite.
awaitToastShown(parent, run, 0);
// the show if it never took, then capture only after a screenshot readback contains
// the rendered top bar. Bounded so a genuine show failure degrades to a capture
// instead of hanging the suite.
awaitToastShown(parent, 0);
}

private void showToast() {
Expand Down Expand Up @@ -101,12 +103,10 @@ public void cleanup() {
private int lastToastY = Integer.MIN_VALUE;
private int lastToastH = Integer.MIN_VALUE;

private void awaitToastShown(final Form parent, final Runnable run, final int waitedMs) {
private void awaitToastShown(final Form parent, final int waitedMs) {
Form f = Display.getInstance().getCurrent();
Object tbc = (f != null) ? f.getClientProperty("ToastBarComponent") : null;
boolean shown = (tbc instanceof Component)
&& ((Component) tbc).isVisible()
&& ((Component) tbc).getHeight() > 0;
boolean shown = isToastComponentShown(tbc);
if (shown) {
Component c = (Component) tbc;
if (c.getY() == lastToastY && c.getHeight() == lastToastH) {
Expand All @@ -122,13 +122,113 @@ private void awaitToastShown(final Form parent, final Runnable run, final int wa
lastToastH = Integer.MIN_VALUE;
}
if ((shown && stableGeometryPolls >= 3) || waitedMs >= 15000) {
run.run();
awaitRenderedToastFrame(parent, 0);
return;
}
if (!shown && waitedMs > 0 && (waitedMs % 3000) == 0) {
// the show did not take (toast left at height 0) -> re-issue it
showToast();
}
UITimer.timer(250, false, parent, () -> awaitToastShown(parent, run, waitedMs + 250));
UITimer.timer(250, false, parent, () -> awaitToastShown(parent, waitedMs + 250));
}

private void awaitRenderedToastFrame(final Form parent, final int waitedMs) {
if (captureBlockedByOrientation("ToastBarTopPosition", waitedMs)) {
UITimer.timer(250, false, parent, () -> awaitRenderedToastFrame(parent, waitedMs + 250));
return;
}

Form f = Display.getInstance().getCurrent();
Object tbc = (f != null) ? f.getClientProperty("ToastBarComponent") : null;
if (tbc instanceof Component) {
Component c = (Component) tbc;
c.repaint();
if (c.getParent() != null) {
c.getParent().revalidate();
c.getParent().repaint();
}
}
parent.repaint();

markCaptureStarted();
Display.getInstance().screenshot(screen -> {
if (screen == null) {
Cn1ssDeviceRunnerHelper.emitPlaceholderScreenshot("ToastBarTopPosition");
done();
return;
}
boolean rendered = containsRenderedToastBar(screen);
if (rendered || waitedMs >= 12000) {
if (!rendered) {
System.out.println("CN1SS:WARN:test=ToastBarTopPosition rendered toast not detected at capture bound");
}
Cn1ssDeviceRunnerHelper.emitImage(screen, "ToastBarTopPosition", this::done);
return;
}
screen.dispose();
if (waitedMs > 0 && (waitedMs % 3000) == 0 && !isToastComponentShown(tbc)) {
showToast();
awaitToastShown(parent, 0);
return;
}
UITimer.timer(250, false, parent, () -> awaitRenderedToastFrame(parent, waitedMs + 250));
});
}

private boolean isToastComponentShown(Object tbc) {
return (tbc instanceof Component)
&& ((Component) tbc).isVisible()
&& ((Component) tbc).getHeight() > 0;
}

private boolean containsRenderedToastBar(Image screen) {
int width = screen.getWidth();
int height = screen.getHeight();
if (width <= 0 || height <= 0) {
return false;
}
int[] rgb = screen.getRGB();
int maxY = Math.min(height, Math.max(1, height / 2));
int requiredDarkPixels = Math.max(1, (width * 3) / 5);
// macOS native text antialiasing leaves relatively few pure-white
// pixels in the toast text/icon, even in the committed golden.
int requiredBrightPixels = Math.max(16, width / 3);
int bestBandHeight = 0;
int bestBandBrightPixels = 0;
int currentBandHeight = 0;
int currentBandBrightPixels = 0;
for (int y = 0; y < maxY; y++) {
int darkPixels = 0;
int brightPixels = 0;
int rowOffset = y * width;
for (int x = 0; x < width; x++) {
int color = rgb[rowOffset + x];
int r = (color >> 16) & 0xff;
int g = (color >> 8) & 0xff;
int b = color & 0xff;
if (r < 96 && g < 96 && b < 96) {
darkPixels++;
}
if (r > 180 && g > 180 && b > 180) {
brightPixels++;
}
}
if (darkPixels >= requiredDarkPixels) {
currentBandHeight++;
currentBandBrightPixels += brightPixels;
} else {
if (currentBandHeight > bestBandHeight) {
bestBandHeight = currentBandHeight;
bestBandBrightPixels = currentBandBrightPixels;
}
currentBandHeight = 0;
currentBandBrightPixels = 0;
}
}
if (currentBandHeight > bestBandHeight) {
bestBandHeight = currentBandHeight;
bestBandBrightPixels = currentBandBrightPixels;
}
return bestBandHeight > 0 && bestBandBrightPixels >= requiredBrightPixels;
}
}
2 changes: 1 addition & 1 deletion scripts/input-validation-app/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ emitted when each gesture fires.

== Suite

Each step waits up to 8 seconds for its expected event, then auto-advances
Each step waits up to 15 seconds for its expected event, then auto-advances
on success or timeout. The XCUITest driver issues the actual OS input at
the expected time.

Expand Down
Loading
Loading