Skip to content

Commit 9134f93

Browse files
committed
Handle WebP AVD skins and narrow CI trigger
1 parent 600e802 commit 9134f93

File tree

2 files changed

+68
-14
lines changed

2 files changed

+68
-14
lines changed

.github/workflows/avd-skin-conversion.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ concurrency:
1313

1414
jobs:
1515
convert:
16+
if: ${{ github.event_name != 'pull_request' || github.event.action != 'synchronize' || github.event.before != '0000000000000000000000000000000000000000' }}
1617
runs-on: ubuntu-latest
1718
steps:
1819
- name: Checkout repository
@@ -27,16 +28,16 @@ jobs:
2728
- name: Download sample AVD skin
2829
run: |
2930
set -euo pipefail
30-
curl -L --fail --retry 5 --retry-delay 5 -o skins.zip https://codeload.github.com/google/android-emulator-skins/zip/refs/heads/master
31+
curl -L --fail --retry 5 --retry-delay 5 -o skins.zip https://github.com/larskristianhaga/Android-emulator-skins/archive/refs/heads/master.zip
3132
mkdir -p skins
32-
unzip -q skins.zip "android-emulator-skins-master/Pixel_4/*" -d skins
33+
unzip -q skins.zip "Android-emulator-skins-master/nexus_10/*" -d skins
3334
3435
- name: Convert AVD skin to Codename One skin
3536
run: |
3637
mkdir -p build
37-
java AvdSkinToCodenameOneSkin.java skins/android-emulator-skins-master/Pixel_4 build/Pixel_4.skin
38+
java AvdSkinToCodenameOneSkin.java skins/Android-emulator-skins-master/nexus_10 build/nexus_10.skin
3839
3940
- name: Validate Codename One skin archive
4041
run: |
41-
test -f build/Pixel_4.skin
42-
unzip -l build/Pixel_4.skin
42+
test -f build/nexus_10.skin
43+
unzip -l build/nexus_10.skin

AvdSkinToCodenameOneSkin.java

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import java.awt.*;
22
import java.awt.image.BufferedImage;
3+
import java.awt.image.ImageObserver;
4+
import java.awt.image.PixelGrabber;
35
import java.io.*;
46
import java.nio.file.*;
57
import java.time.LocalDateTime;
@@ -31,6 +33,8 @@ public class AvdSkinToCodenameOneSkin {
3133
private static final double TABLET_INCH_THRESHOLD = 6.5d;
3234

3335
public static void main(String[] args) throws Exception {
36+
System.setProperty("java.awt.headless", "true");
37+
3438
if (args.length == 0 || args.length > 2) {
3539
System.err.println("Usage: java AvdSkinToCodenameOneSkin.java <avd-skin-dir> [output.skin]");
3640
System.exit(1);
@@ -132,10 +136,7 @@ private static DeviceImages buildDeviceImages(Path skinDir, OrientationInfo orie
132136
throw new IllegalStateException("Missing image '" + orientation.imageName() + "' for " + orientation.orientation());
133137
}
134138
try {
135-
BufferedImage original = javax.imageio.ImageIO.read(imagePath.toFile());
136-
if (original == null) {
137-
throw new IllegalStateException("Failed to decode image " + imagePath);
138-
}
139+
BufferedImage original = readImage(imagePath);
139140
if (orientation.display().width() <= 0 || orientation.display().height() <= 0) {
140141
throw new IllegalStateException("Invalid display dimensions for " + orientation.orientation());
141142
}
@@ -145,6 +146,53 @@ private static DeviceImages buildDeviceImages(Path skinDir, OrientationInfo orie
145146
}
146147
}
147148

149+
private static BufferedImage readImage(Path imagePath) throws IOException {
150+
BufferedImage standard = javax.imageio.ImageIO.read(imagePath.toFile());
151+
if (standard != null) {
152+
return standard;
153+
}
154+
155+
byte[] data = Files.readAllBytes(imagePath);
156+
Image toolkitImage;
157+
try {
158+
toolkitImage = Toolkit.getDefaultToolkit().createImage(data);
159+
} catch (HeadlessException err) {
160+
throw new IllegalStateException("Unsupported image format for " + imagePath + " (headless toolkit)", err);
161+
}
162+
if (toolkitImage == null) {
163+
throw new IllegalStateException("Unsupported image format for " + imagePath);
164+
}
165+
PixelGrabber grabber = new PixelGrabber(toolkitImage, 0, 0, -1, -1, true);
166+
try {
167+
grabber.grabPixels();
168+
} catch (InterruptedException err) {
169+
Thread.currentThread().interrupt();
170+
throw new IOException("Interrupted while decoding " + imagePath, err);
171+
}
172+
if (grabber.getStatus() != ImageObserver.ALLBITS) {
173+
throw new IllegalStateException("Failed to decode image " + imagePath);
174+
}
175+
int width = grabber.getWidth();
176+
int height = grabber.getHeight();
177+
if (width <= 0 || height <= 0) {
178+
throw new IllegalStateException("Failed to decode image " + imagePath);
179+
}
180+
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
181+
Object pixels = grabber.getPixels();
182+
if (!(pixels instanceof int[] rgb)) {
183+
throw new IllegalStateException("Unsupported pixel model in " + imagePath);
184+
}
185+
Graphics2D g = result.createGraphics();
186+
try {
187+
g.setComposite(AlphaComposite.Src);
188+
g.drawImage(toolkitImage, 0, 0, null);
189+
result.setRGB(0, 0, width, height, rgb, 0, width);
190+
} finally {
191+
g.dispose();
192+
}
193+
return result;
194+
}
195+
148196
private static void writeEntry(ZipOutputStream zos, String name, BufferedImage image) throws IOException {
149197
ZipEntry entry = new ZipEntry(name);
150198
zos.putNextEntry(entry);
@@ -273,7 +321,7 @@ private void handleKeyValue(String line) {
273321
String key = parts[0];
274322
String value = unquote(parts[1]);
275323
String ctxName = ctx.name.toLowerCase(Locale.ROOT);
276-
if (ctxName.contains("image") && key.equalsIgnoreCase("name")) {
324+
if (ctxName.contains("image") && isImageKey(key)) {
277325
builder.considerImage(value, contextStack, this::resolveImagePath);
278326
} else if (ctxName.contains("display")) {
279327
switch (key.toLowerCase(Locale.ROOT)) {
@@ -285,6 +333,11 @@ private void handleKeyValue(String line) {
285333
}
286334
}
287335

336+
private boolean isImageKey(String key) {
337+
String lower = key.toLowerCase(Locale.ROOT);
338+
return lower.equals("name") || lower.equals("image") || lower.equals("filename");
339+
}
340+
288341
private void pushContext(String name) {
289342
name = name.trim();
290343
if (name.isEmpty()) {
@@ -409,18 +462,18 @@ static ImageCandidate from(String name, Deque<Context> contexts, java.util.funct
409462
boolean controlHint = false;
410463
for (Context ctx : contexts) {
411464
String lower = ctx.name.toLowerCase(Locale.ROOT);
412-
if (lower.contains("button") || lower.contains("control") || lower.contains("icon") || lower.contains("touch")) {
465+
if (lower.contains("button") || lower.contains("control") || lower.contains("icon") || lower.contains("touch") || lower.contains("shadow") || lower.contains("onion")) {
413466
controlHint = true;
414467
}
415-
if (lower.contains("device") || lower.contains("frame") || lower.contains("skin") || lower.contains("phone") || lower.contains("tablet")) {
468+
if (lower.contains("device") || lower.contains("frame") || lower.contains("skin") || lower.contains("phone") || lower.contains("tablet") || lower.contains("background") || lower.contains("back")) {
416469
frameHint = true;
417470
}
418471
}
419472
String lowerName = name.toLowerCase(Locale.ROOT);
420-
if (lowerName.contains("frame") || lowerName.contains("device") || lowerName.contains("shell") || lowerName.contains("body")) {
473+
if (lowerName.contains("frame") || lowerName.contains("device") || lowerName.contains("shell") || lowerName.contains("body") || lowerName.contains("background") || lowerName.contains("back") || lowerName.contains("fore")) {
421474
frameHint = true;
422475
}
423-
if (lowerName.contains("button") || lowerName.contains("control") || lowerName.contains("icon")) {
476+
if (lowerName.contains("button") || lowerName.contains("control") || lowerName.contains("icon") || lowerName.contains("shadow") || lowerName.contains("onion")) {
424477
controlHint = true;
425478
}
426479
long area = computeArea(resolver.apply(name));

0 commit comments

Comments
 (0)