blob: 6d72538ceefb6dbf5c4cb65b429079368f856a79 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.ddms.screenshot;
import com.android.SdkConstants;
import com.android.dvlib.DeviceSchemaTest;
import com.android.resources.ScreenOrientation;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.DeviceParser;
import com.android.testutils.SdkTestCase;
import com.android.tools.idea.rendering.ImageUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.io.Files;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import junit.framework.TestCase;
import org.jetbrains.android.AndroidTestBase;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import static com.android.tools.idea.ddms.screenshot.DeviceArtPainter.DeviceData;
import static com.android.tools.idea.ddms.screenshot.DeviceArtPainter.FrameData;
public class DeviceArtPainterTest extends TestCase {
public void testGenerateCropData() throws Exception {
// TODO: Assert that the crop data is right
generateCropData();
}
public void testRendering() throws Exception {
// This test is disabled but code is preserved here; this is handy for quickly checking rendering results
// when tweaking the code to assemble composite images. (Make sure you also turn off the thumbnail cache first!
//noinspection ConstantConditions
if (false) {
DeviceArtPainter framePainter = DeviceArtPainter.getInstance();
for (DeviceArtDescriptor spec : framePainter.getDescriptors()) {
if ("wear_round".equals(spec.getId())) {
FrameData frameData = new DeviceData(null, spec).getFrameData(ScreenOrientation.LANDSCAPE, 320);
BufferedImage image = frameData.getImage(true);
@SuppressWarnings("SSBasedInspection")
File file = File.createTempFile("test-rendering", "png");
if (file.exists()) {
boolean deleted = file.delete();
assertTrue(deleted);
}
ImageIO.write(image, "PNG", file);
if (file.exists() && SystemInfo.isMac) {
Runtime.getRuntime().exec("/usr/bin/open " + file.getPath());
}
}
}
}
}
public void testCroppedRendering() throws Exception {
File deviceArtPath = new File(AndroidTestBase.getAbsoluteTestDataPath(), FileUtil.join("..", "device-art-resources"));
List<DeviceArtDescriptor> descriptors = DeviceArtDescriptor.getDescriptors(new File[]{deviceArtPath});
DeviceArtDescriptor wear_square = findDescriptor(descriptors, "wear_square");
DeviceArtDescriptor wear_round = findDescriptor(descriptors, "wear_round");
assertNotNull(wear_square);
assertNotNull(wear_round);
Dimension size = wear_round.getScreenSize(ScreenOrientation.LANDSCAPE);
BufferedImage sample = createSampleImage(size, Color.RED);
BufferedImage framed = DeviceArtPainter.createFrame(sample, wear_round, true, false);
// make sure that a location outside the round frame is empty
// (if the mask was not applied, this would be the same color as the source image)
Point loc = wear_round.getScreenPos(ScreenOrientation.LANDSCAPE);
int c = framed.getRGB(loc.x, loc.y);
assertEquals(0x0, c);
// a point at the center should be the same as the source
c = framed.getRGB(loc.x + size.width / 2, loc.y + size.height / 2);
assertEquals(Color.RED.getRGB(), c);
}
@NotNull
private static BufferedImage createSampleImage(Dimension size, Color color) {
@SuppressWarnings("UndesirableClassUsage") // no need to support retina
BufferedImage img = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setColor(color);
g2d.fillRect(0, 0, size.width, size.height);
g2d.dispose();
return img;
}
@Nullable
private static DeviceArtDescriptor findDescriptor(@NotNull List<DeviceArtDescriptor> descriptors, @NotNull String id) {
for (DeviceArtDescriptor desc : descriptors) {
if (desc.getId().equals(id)) {
return desc;
}
}
return null;
}
public void generateCropData() throws Exception {
DeviceArtPainter framePainter = DeviceArtPainter.getInstance();
Device device = newDevice();
for (DeviceArtDescriptor spec : framePainter.getDescriptors()) {
DeviceData data = new DeviceData(device, spec);
Rectangle cropRect = spec.getCrop(ScreenOrientation.LANDSCAPE);
if (spec.getName().startsWith("Generic ")) {
// No crop data for generic nine patches since they are stretchable
continue;
}
if (cropRect != null && !cropRect.getSize().equals(spec.getScreenSize(ScreenOrientation.LANDSCAPE))) {
// Already have crop data for this spec; skipping
continue;
}
if (spec.getName().startsWith("Android TV")) {
// These images are already cropped
continue;
}
System.out.println("for spec " + spec.getName() + " -- " + spec.getId());
FrameData landscapeData = data.getFrameData(ScreenOrientation.LANDSCAPE, Integer.MAX_VALUE);
// Must use computeImage rather than getImage here since we want to get the
// full size images, not the already cropped images
BufferedImage effectsImage;
Rectangle crop;
ImageUtils.CropFilter filter = new ImageUtils.CropFilter() {
@Override
public boolean crop(BufferedImage bufferedImage, int x, int y) {
int rgb = bufferedImage.getRGB(x, y);
return ((rgb & 0xFF000000) >>> 24) < 2;
}
};
FrameData portraitData = data.getFrameData(ScreenOrientation.PORTRAIT, Integer.MAX_VALUE);
try {
effectsImage = portraitData.computeImage(true, 0, 0, portraitData.getFrameWidth(), portraitData.getFrameHeight());
} catch (OutOfMemoryError oome) {
// This test sometimes fails on the build server because it runs out of memory; it's a memory
// hungry test which sometimes fails when run as part of thousands of other tests.
// Ignore those types of failures.
// Make sure it's not failing to allocate memory due to some crazy large bounds we didn't anticipate:
assertTrue(portraitData.getFrameWidth() < 4000);
assertTrue(portraitData.getFrameHeight() < 4000);
return;
}
assertNotNull(effectsImage);
crop = ImageUtils.getCropBounds(effectsImage, filter, null);
assertNotNull(crop);
System.out.print(" port crop=\"");
System.out.print(crop.x);
System.out.print(",");
System.out.print(crop.y);
System.out.print(",");
System.out.print(crop.width);
System.out.print(",");
System.out.print(crop.height);
System.out.println("\"");
try {
effectsImage = landscapeData.computeImage(true, 0, 0, landscapeData.getFrameWidth(), landscapeData.getFrameHeight());
} catch (OutOfMemoryError oome) {
// See portrait case above
assertTrue(landscapeData.getFrameWidth() < 4000);
assertTrue(landscapeData.getFrameHeight() < 4000);
return;
}
assertNotNull(effectsImage);
crop = ImageUtils.getCropBounds(effectsImage, filter, null);
assertNotNull(crop);
System.out.print(" landscape crop=\"");
System.out.print(crop.x);
System.out.print(",");
System.out.print(crop.y);
System.out.print(",");
System.out.print(crop.width);
System.out.print(",");
System.out.print(crop.height);
System.out.println("\"");
}
}
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
private static Device newDevice() throws Exception {
Collection<Device> devices;
InputStream stream = null;
try {
stream = DeviceSchemaTest.class.getResourceAsStream("devices_minimal.xml");
devices = DeviceParser.parse(stream).values();
} finally {
if (stream != null) {
stream.close();
}
}
assertTrue(!devices.isEmpty());
return devices.iterator().next();
}
@Nullable
private static BufferedImage getImage(@NotNull File srcDir, @Nullable File file) {
if (file == null) {
return null;
}
if (!file.isAbsolute()) {
file = new File(srcDir, file.getPath());
}
if (file.exists()) {
try {
return ImageIO.read(file);
}
catch (IOException e) {
// pass
}
}
return null;
}
public void testCropData() throws Exception {
//noinspection ConstantConditions
if (true) {
// This test no longer applies; it was used to convert assets with a lot of padding into more tightly cropped
// screenshots. We're preserving the code since for future device releases we might get new artwork which includes
// padding.
return;
}
// Apply crop
DeviceArtPainter framePainter = DeviceArtPainter.getInstance();
Device device = newDevice();
File srcDir = DeviceArtDescriptor.getBundledDescriptorsFolder();
SdkTestCase.getTempDir();
File destDir = new File(SdkTestCase.getTempDir(), "device-art");
if (!destDir.exists()) {
boolean ok = destDir.mkdirs();
assertTrue(ok);
}
StringBuilder sb = new StringBuilder(1000);
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<!-- Copyright (C) 2013 The Android Open Source Project\n" +
"\n" +
" Licensed under the Apache License, Version 2.0 (the \"License\");\n" +
" you may not use this file except in compliance with the License.\n" +
" You may obtain a copy of the License at\n" +
"\n" +
" http://www.apache.org/licenses/LICENSE-2.0\n" +
"\n" +
" Unless required by applicable law or agreed to in writing, software\n" +
" distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
" See the License for the specific language governing permissions and\n" +
" limitations under the License.\n" +
"-->\n" +
"<devices>\n" +
"\n");
for (DeviceArtDescriptor spec : framePainter.getDescriptors()) {
sb.append(" <device id=\"");
sb.append(spec.getId());
sb.append("\" name=\"");
sb.append(spec.getName());
sb.append("\">\n");
DeviceData deviceData = new DeviceData(device, spec);
for (ScreenOrientation orientation : ScreenOrientation.values()) {
if (orientation == ScreenOrientation.SQUARE) {
continue;
}
if (orientation != ScreenOrientation.LANDSCAPE && spec.getId().startsWith("tv_")) {
// Android TV only uses landscape orientation
continue;
}
Rectangle cropRect = spec.getCrop(orientation);
sb.append(" <orientation name=\"");
sb.append(orientation.getResourceValue());
sb.append("\" ");
DeviceArtDescriptor descriptor = deviceData.getDescriptor();
if (spec.getName().startsWith("Generic ") || cropRect == null || spec.getName().startsWith("Android TV")) {
System.out.println("Nothing to do for " + spec.getId() + " orientation " + orientation);
cropRect = new Rectangle(0, 0, descriptor.getFrameSize(orientation).width, descriptor.getFrameSize(orientation).height);
}
sb.append("size=\"");
sb.append(Integer.toString(cropRect.width));
sb.append(",");
sb.append(Integer.toString(cropRect.height));
sb.append("\" screenPos=\"");
sb.append(Integer.toString(descriptor.getScreenPos(orientation).x - cropRect.x));
sb.append(",");
sb.append(Integer.toString(descriptor.getScreenPos(orientation).y - cropRect.y));
sb.append("\" screenSize=\"");
sb.append(Integer.toString(descriptor.getScreenSize(orientation).width));
sb.append(",");
sb.append(Integer.toString(descriptor.getScreenSize(orientation).height));
sb.append("\"");
if (descriptor.getDropShadow(orientation) != null) {
sb.append(" shadow=\"");
//noinspection ConstantConditions
sb.append(descriptor.getDropShadow(orientation).getName());
sb.append("\"");
}
if (descriptor.getFrame(orientation) != null) {
sb.append(" back=\"");
//noinspection ConstantConditions
sb.append(descriptor.getFrame(orientation).getName());
sb.append("\"");
}
if (descriptor.getReflectionOverlay(orientation) != null) {
sb.append(" lights=\"");
//noinspection ConstantConditions
sb.append(descriptor.getReflectionOverlay(orientation).getName());
sb.append("\"");
}
if (descriptor.getMask(orientation) != null) {
sb.append(" mask=\"");
//noinspection ConstantConditions
sb.append(descriptor.getMask(orientation).getName());
sb.append("\"");
}
sb.append("/>\n");
// Must use computeImage rather than getImage here since we want to get the
// full size images, not the already cropped images
writeCropped(srcDir, destDir, spec, cropRect, descriptor.getFrame(orientation));
writeCropped(srcDir, destDir, spec, cropRect, descriptor.getDropShadow(orientation));
writeCropped(srcDir, destDir, spec, cropRect, descriptor.getReflectionOverlay(orientation));
writeCropped(srcDir, destDir, spec, cropRect, descriptor.getMask(orientation));
}
// (3) Rewrite emulator skin file
File layoutFile = new File(srcDir, spec.getId() + File.separator + SdkConstants.FN_SKIN_LAYOUT);
if (layoutFile.exists() && !spec.getId().startsWith("tv_")) { // no crop data in tv (and lack of portrait fails below)
String layout = Files.toString(layoutFile, Charsets.UTF_8);
final Rectangle portraitCrop = spec.getCrop(ScreenOrientation.PORTRAIT);
assertNotNull("No crop data found; did you run this test on an already processed device-art.xml?", portraitCrop);
final Rectangle landscapeCrop = spec.getCrop(ScreenOrientation.LANDSCAPE);
layout = replace(layout, new String[]{"layouts {", "portrait {", "width "}, new Function<Integer, Integer>() {
@Override
public Integer apply(@Nullable Integer input) {
return portraitCrop.width;
}
});
layout = replace(layout, new String[]{"layouts {", "portrait {", "height "}, new Function<Integer, Integer>() {
@Override
public Integer apply(@Nullable Integer input) {
return portraitCrop.height;
}
});
layout = replace(layout, new String[]{"layouts {", "portrait {", "part2 {", "x "}, new Function<Integer, Integer>() {
@Override
public Integer apply(@Nullable Integer input) {
//noinspection ConstantConditions
return input - portraitCrop.x;
}
});
layout = replace(layout, new String[]{"layouts {", "portrait {", "part2 {", "y "}, new Function<Integer, Integer>() {
@Override
public Integer apply(@Nullable Integer input) {
//noinspection ConstantConditions
return input - portraitCrop.y;
}
});
// landscape
layout = replace(layout, new String[]{"layouts {", "landscape {", "width "}, new Function<Integer, Integer>() {
@Override
public Integer apply(@Nullable Integer input) {
return landscapeCrop.width;
}
});
layout = replace(layout, new String[]{"layouts {", "landscape {", "height "}, new Function<Integer, Integer>() {
@Override
public Integer apply(@Nullable Integer input) {
return landscapeCrop.height;
}
});
layout = replace(layout, new String[]{"layouts {", "landscape {", "part2 {", "x "}, new Function<Integer, Integer>() {
@Override
public Integer apply(@Nullable Integer input) {
//noinspection ConstantConditions
return input - landscapeCrop.x;
}
});
layout = replace(layout, new String[]{"layouts {", "landscape {", "part2 {", "y "}, new Function<Integer, Integer>() {
@Override
public Integer apply(@Nullable Integer input) {
//noinspection ConstantConditions
return input - landscapeCrop.y;
}
});
File outputLayoutFile = new File(destDir, spec.getId() + File.separator + SdkConstants.FN_SKIN_LAYOUT);
if (!outputLayoutFile.getParentFile().exists()) {
boolean mkdirs = outputLayoutFile.getParentFile().mkdirs();
assertTrue(mkdirs);
}
Files.write(layout, outputLayoutFile, Charsets.UTF_8);
}
sb.append(" </device>\n\n");
}
sb.append("\n</devices>\n");
File deviceArt = new File(destDir, "device-art.xml");
Files.write(sb.toString(), deviceArt, Charsets.UTF_8);
System.out.println("Wrote device art file " + deviceArt);
}
private static String replace(String file, String[] sections, Function<Integer, Integer> replace) {
int index = 0;
for (String section : sections) {
index = file.indexOf(section, index);
assert index != -1 : section + " not found";
index += section.length();
}
// We're now pointing to a token
int lineEnd = file.indexOf('\n', index);
assert lineEnd != -1;
String word = file.substring(index, lineEnd);
int input = Integer.parseInt(word);
@SuppressWarnings("ConstantConditions")
int replaced = replace.apply(input);
return file.substring(0, index) + Integer.toString(replaced) + file.substring(lineEnd);
}
private static void writeCropped(File srcDir, File destDir, DeviceArtDescriptor spec, Rectangle cropRect, @Nullable File imageFile)
throws IOException {
if (imageFile == null) {
// This image doesn't apply
return;
}
BufferedImage source = getImage(srcDir, imageFile);
if (source == null) {
return;
}
BufferedImage cropped = cropImage(source, cropRect);
assertNotNull(cropped);
File dir = new File(destDir, spec.getId());
if (!dir.exists()) {
boolean ok = dir.mkdir();
assertTrue(dir.getPath(), ok);
}
ImageIO.write(cropped, "PNG", new File(dir, imageFile.getName()));
}
private static BufferedImage cropImage(@NotNull BufferedImage image, @NotNull Rectangle cropBounds) {
int x1 = cropBounds.x;
int y1 = cropBounds.y;
int width = cropBounds.width;
int height = cropBounds.height;
int x2 = x1 + width;
int y2 = y1 + height;
// Now extract the sub-image
@SuppressWarnings("UndesirableClassUsage")
BufferedImage cropped = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g = cropped.getGraphics();
//noinspection UseJBColor
g.setColor(new Color(0, true));
g.fillRect(0, 0, width, height);
g.drawImage(image, 0, 0, width, height, x1, y1, x2, y2, null);
g.dispose();
return cropped;
}
}