Skip to content

Commit ee32dd3

Browse files
committed
Improve AVD layout parsing and CI workflow
1 parent 83fb592 commit ee32dd3

File tree

2 files changed

+108
-13
lines changed

2 files changed

+108
-13
lines changed

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
name: AVD Skin Conversion
22

33
on:
4-
push:
5-
paths:
6-
- '.github/workflows/avd-skin-conversion.yml'
7-
- 'AvdSkinToCodenameOneSkin.java'
84
pull_request:
95
paths:
106
- '.github/workflows/avd-skin-conversion.yml'
117
- 'AvdSkinToCodenameOneSkin.java'
8+
workflow_dispatch:
129

1310
jobs:
1411
convert:
@@ -25,7 +22,7 @@ jobs:
2522

2623
- name: Download sample AVD skin
2724
run: |
28-
curl -L -o skins.zip https://github.com/google/android-emulator-skins/archive/refs/heads/main.zip
25+
curl -L --fail -o skins.zip https://codeload.github.com/google/android-emulator-skins/zip/refs/heads/main
2926
mkdir -p skins
3027
unzip -q skins.zip "android-emulator-skins-main/Pixel_4/*" -d skins
3128

AvdSkinToCodenameOneSkin.java

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public static void main(String[] args) throws Exception {
5252
error("Output file %s already exists".formatted(outputFile));
5353
}
5454

55-
LayoutInfo layoutInfo = LayoutInfo.parse(findLayoutFile(skinDirectory));
55+
Path layoutFile = findLayoutFile(skinDirectory);
56+
LayoutInfo layoutInfo = LayoutInfo.parse(layoutFile, skinDirectory);
5657
HardwareInfo hardwareInfo = HardwareInfo.parse(skinDirectory.resolve("hardware.ini"));
5758

5859
if (!layoutInfo.hasBothOrientations()) {
@@ -211,9 +212,9 @@ boolean hasBothOrientations() {
211212
return portrait != null && landscape != null;
212213
}
213214

214-
static LayoutInfo parse(Path layoutFile) {
215+
static LayoutInfo parse(Path layoutFile, Path skinDirectory) {
215216
try {
216-
return new LayoutParser().parse(Files.readString(layoutFile));
217+
return new LayoutParser(layoutFile, skinDirectory).parse(Files.readString(layoutFile));
217218
} catch (IOException err) {
218219
throw new UncheckedIOException("Failed to read layout file " + layoutFile, err);
219220
}
@@ -223,6 +224,13 @@ static LayoutInfo parse(Path layoutFile) {
223224
private static class LayoutParser {
224225
private final EnumMap<OrientationType, OrientationInfoBuilder> builders = new EnumMap<>(OrientationType.class);
225226
private final Deque<Context> contextStack = new ArrayDeque<>();
227+
private final Path skinDirectory;
228+
private final Path layoutParent;
229+
230+
LayoutParser(Path layoutFile, Path skinDirectory) {
231+
this.skinDirectory = skinDirectory;
232+
this.layoutParent = layoutFile.getParent();
233+
}
226234

227235
LayoutInfo parse(String text) {
228236
String[] lines = text.split("\r?\n");
@@ -263,10 +271,10 @@ private void handleKeyValue(String line) {
263271
return;
264272
}
265273
String key = parts[0];
266-
String value = parts[1];
274+
String value = unquote(parts[1]);
267275
String ctxName = ctx.name.toLowerCase(Locale.ROOT);
268276
if (ctxName.contains("image") && key.equalsIgnoreCase("name")) {
269-
builder.imageName = value;
277+
builder.considerImage(value, contextStack, this::resolveImagePath);
270278
} else if (ctxName.contains("display")) {
271279
switch (key.toLowerCase(Locale.ROOT)) {
272280
case "x" -> builder.displayX = parseInt(value);
@@ -311,6 +319,17 @@ private String[] splitKeyValue(String line) {
311319
return parts;
312320
}
313321

322+
private String unquote(String value) {
323+
value = value.trim();
324+
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
325+
return value.substring(1, value.length() - 1);
326+
}
327+
if (value.length() >= 2 && value.startsWith("'") && value.endsWith("'")) {
328+
return value.substring(1, value.length() - 1);
329+
}
330+
return value;
331+
}
332+
314333
private int parseInt(String value) {
315334
try {
316335
return Integer.parseInt(value);
@@ -319,6 +338,20 @@ private int parseInt(String value) {
319338
}
320339
}
321340

341+
private Path resolveImagePath(String name) {
342+
Path candidate = skinDirectory.resolve(name).normalize();
343+
if (Files.isRegularFile(candidate)) {
344+
return candidate;
345+
}
346+
if (layoutParent != null) {
347+
Path sibling = layoutParent.resolve(name).normalize();
348+
if (Files.isRegularFile(sibling)) {
349+
return sibling;
350+
}
351+
}
352+
return candidate;
353+
}
354+
322355
private String stripComments(String line) {
323356
int slash = line.indexOf("//");
324357
int hash = line.indexOf('#');
@@ -349,17 +382,82 @@ private OrientationType detectOrientation(String name) {
349382
private record Context(String name, OrientationType orientation) {}
350383

351384
private static class OrientationInfoBuilder {
352-
String imageName;
385+
ImageCandidate selectedImage;
353386
Integer displayX;
354387
Integer displayY;
355388
Integer displayWidth;
356389
Integer displayHeight;
357390

391+
void considerImage(String name, Deque<Context> contexts, java.util.function.Function<String, Path> resolver) {
392+
ImageCandidate candidate = ImageCandidate.from(name, contexts, resolver);
393+
if (selectedImage == null || candidate.isBetterThan(selectedImage)) {
394+
selectedImage = candidate;
395+
}
396+
}
397+
358398
OrientationInfo build(OrientationType type) {
359-
if (imageName == null || displayX == null || displayY == null || displayWidth == null || displayHeight == null) {
399+
if (selectedImage == null || displayX == null || displayY == null || displayWidth == null || displayHeight == null) {
360400
throw new IllegalStateException("Layout definition for " + type + " is incomplete");
361401
}
362-
return new OrientationInfo(type, imageName, new DisplayArea(displayX, displayY, displayWidth, displayHeight));
402+
return new OrientationInfo(type, selectedImage.name(), new DisplayArea(displayX, displayY, displayWidth, displayHeight));
403+
}
404+
}
405+
406+
private record ImageCandidate(String name, long area, boolean frameHint, boolean controlHint) {
407+
static ImageCandidate from(String name, Deque<Context> contexts, java.util.function.Function<String, Path> resolver) {
408+
boolean frameHint = false;
409+
boolean controlHint = false;
410+
for (Context ctx : contexts) {
411+
String lower = ctx.name.toLowerCase(Locale.ROOT);
412+
if (lower.contains("button") || lower.contains("control") || lower.contains("icon") || lower.contains("touch")) {
413+
controlHint = true;
414+
}
415+
if (lower.contains("device") || lower.contains("frame") || lower.contains("skin") || lower.contains("phone") || lower.contains("tablet")) {
416+
frameHint = true;
417+
}
418+
}
419+
String lowerName = name.toLowerCase(Locale.ROOT);
420+
if (lowerName.contains("frame") || lowerName.contains("device") || lowerName.contains("shell") || lowerName.contains("body")) {
421+
frameHint = true;
422+
}
423+
if (lowerName.contains("button") || lowerName.contains("control") || lowerName.contains("icon")) {
424+
controlHint = true;
425+
}
426+
long area = computeArea(resolver.apply(name));
427+
return new ImageCandidate(name, area, frameHint, controlHint);
428+
}
429+
430+
private static long computeArea(Path imagePath) {
431+
if (imagePath == null || !Files.isRegularFile(imagePath)) {
432+
return -1;
433+
}
434+
try {
435+
BufferedImage img = javax.imageio.ImageIO.read(imagePath.toFile());
436+
if (img == null) {
437+
return -1;
438+
}
439+
return (long) img.getWidth() * (long) img.getHeight();
440+
} catch (IOException err) {
441+
return -1;
442+
}
443+
}
444+
445+
boolean isBetterThan(ImageCandidate other) {
446+
if (other == null) {
447+
return true;
448+
}
449+
if (frameHint != other.frameHint) {
450+
return frameHint && !controlHint;
451+
}
452+
if (controlHint != other.controlHint) {
453+
return !controlHint;
454+
}
455+
long thisArea = Math.max(area, 0);
456+
long otherArea = Math.max(other.area, 0);
457+
if (thisArea != otherArea) {
458+
return thisArea > otherArea;
459+
}
460+
return name.compareTo(other.name) < 0;
363461
}
364462
}
365463
}

0 commit comments

Comments
 (0)