/* * 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.resources.ScreenOrientation; import com.android.tools.idea.rendering.ImageUtils; import com.android.utils.XmlUtils; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.io.Files; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.containers.HashSet; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import static com.android.tools.lint.detector.api.LintUtils.getChildren; /** * Descriptor for a device frame picture (background, shadow, reflection) which can be * painted around a screenshot or device rendering */ public class DeviceArtDescriptor { @NotNull public static final DeviceArtDescriptor NONE = new DeviceArtDescriptor(null, null); private final String myId; private final String myName; private final File myFolder; private OrientationData myPortrait; private OrientationData myLandscape; @NonNls private static final String FN_BASE = "device-art-resources"; @NonNls private static final String FN_DESCRIPTOR = "device-art.xml"; /** Returns the absolute path to {@link #FN_BASE} folder, or null if it couldn't be located. */ @Nullable private static File getBundledDescriptorsFolder() { // In the IDE distribution, this should be in plugins/android/lib/FN_BASE String androidJarPath = PathManager.getJarPathForClass(DeviceArtDescriptor.class); if (androidJarPath != null) { File androidJar = new File(androidJarPath); if (androidJar.isFile()) { File base = new File(androidJar.getParentFile(), FN_BASE); if (base.exists() && base.isDirectory()) { return base; } } } // In development environments, search a few other folders String basePath = PathManager.getHomePath(); String[] paths = new String[] { "plugins" + File.separatorChar + "android" + File.separatorChar, ".." + File.separator + "adt" + File.separator + "idea" + File.separator + "android" + File.separatorChar, "android" + File.separatorChar + "android" + File.separatorChar, }; for (String p : paths) { File base = new File(basePath, p); if (base.isDirectory()) { File files = new File(base, FN_BASE); if (files.isDirectory()) { return files; } } } return null; } @Nullable private static File getDescriptorFile(@NotNull File folder) { File file = new File(folder, FN_DESCRIPTOR); return file.isFile() ? file : null; } private static List<File> getDescriptorFiles(@Nullable File[] additionalRoots) { Set<File> roots = new HashSet<File>(); File base = getBundledDescriptorsFolder(); if (base != null) { roots.add(base); } if (additionalRoots != null) { Collections.addAll(roots, additionalRoots); } List<File> files = new ArrayList<File>(roots.size()); for (File root : roots) { File file = getDescriptorFile(root); if (file != null) { files.add(file); } } return files; } public static List<DeviceArtDescriptor> getDescriptors(@Nullable File[] folders) { List<File> files = getDescriptorFiles(folders); List<DeviceArtDescriptor> result = Lists.newArrayList(); for (File file : files) try { String xml = Files.toString(file, Charsets.UTF_8); Document document = XmlUtils.parseDocumentSilently(xml, false); if (document != null) { File baseFolder = file.getParentFile(); addDescriptors(result, document, baseFolder); } else { Logger.getInstance(DeviceArtDescriptor.class).error("Couldn't parse " + file); } } catch (IOException e) { Logger.getInstance(DeviceArtDescriptor.class).error(e); } return result; } private DeviceArtDescriptor(@Nullable File baseFolder, @Nullable Element element) { if (element == null) { myId = myName = ""; myFolder = null; } else { myId = element.getAttribute(SdkConstants.ATTR_ID); myName = element.getAttribute(SdkConstants.ATTR_NAME); myFolder = new File(baseFolder, myId); List<Element> children = getChildren(element); for (Element child : children) { OrientationData orientation = new OrientationData(this, child); if (orientation.isPortrait()) { myPortrait = orientation; } else { myLandscape = orientation; } } } } static void addDescriptors(List<DeviceArtDescriptor> result, Document document, File baseFolder) { NodeList deviceList = document.getElementsByTagName("device"); for (int i = 0; i < deviceList.getLength(); i++) { Element element = (Element)deviceList.item(i); DeviceArtDescriptor descriptor = new DeviceArtDescriptor(baseFolder, element); result.add(descriptor); } } @NotNull public String getId() { return myId; } @NotNull public String getName() { return myName; } @NotNull public OrientationData getArtDescriptor(@NotNull ScreenOrientation orientation) { return orientation == ScreenOrientation.PORTRAIT ? myPortrait : myLandscape; } public File getBaseFolder() { return myFolder; } public Dimension getScreenSize(@NotNull ScreenOrientation orientation) { return getArtDescriptor(orientation).getScreenSize(); } public Point getScreenPos(@NotNull ScreenOrientation orientation) { return getArtDescriptor(orientation).getScreenPos(); } public Dimension getFrameSize(@NotNull ScreenOrientation orientation) { return getArtDescriptor(orientation).getFrameSize(); } public Rectangle getCrop(@NotNull ScreenOrientation orientation) { return getArtDescriptor(orientation).getCrop(); } @Nullable public File getFrame(@NotNull ScreenOrientation orientation) { return getArtDescriptor(orientation).getBackgroundFile(); } @Nullable public File getDropShadow(@NotNull ScreenOrientation orientation) { return getArtDescriptor(orientation).getShadowFile(); } @Nullable public File getReflectionOverlay(@NotNull ScreenOrientation orientation) { return getArtDescriptor(orientation).getReflectionFile(); } @Nullable public File getMask(@NotNull ScreenOrientation orientation) { return getArtDescriptor(orientation).getMaskFile(); } public double getAspectRatio(ScreenOrientation orientation) { return getArtDescriptor(orientation).getAspectRatio(); } public boolean isStretchable() { return myId.equals("phone") || myId.equals("tablet"); } /** Returns whether this descriptor can frame the given image. */ public boolean canFrameImage(BufferedImage image, ScreenOrientation orientation) { if (isStretchable()) { return true; } // Not all devices are available in all orientations if (orientation == ScreenOrientation.PORTRAIT && myPortrait == null) { return false; } else if (orientation == ScreenOrientation.LANDSCAPE && myLandscape == null) { return false; } // Don't support framing images smaller than our screen size (we don't want to stretch the image) Dimension screenSize = getArtDescriptor(orientation).getScreenSize(); if (image.getWidth() < screenSize.getWidth() || image.getHeight() < screenSize.getHeight()) { return false; } // Make sure that the aspect ratio is nearly identical to the image aspect ratio double imgAspectRatio = image.getWidth() / (double) image.getHeight(); double descriptorAspectRatio = getAspectRatio(orientation); return Math.abs(imgAspectRatio - descriptorAspectRatio) < ImageUtils.EPSILON; } /** Descriptor for a particular device frame (e.g. a set of images for a particular device in a particular orientation) */ private static class OrientationData { private final DeviceArtDescriptor myDevice; private final String myShadowName; private final String myBackgroundName; private final String myReflectionName; private final String myMaskName; private final Dimension myScreenSize; private final Point myScreenPos; private final Dimension myFrameSize; private final Rectangle myCrop; private final ScreenOrientation myOrientation; OrientationData(DeviceArtDescriptor device, Element element) { myDevice = device; String orientation = element.getAttribute(SdkConstants.ATTR_NAME); if ("port".equals(orientation)) { myOrientation = ScreenOrientation.PORTRAIT; } else { assert "land".equals(orientation) : orientation; myOrientation = ScreenOrientation.LANDSCAPE; } myFrameSize = getDimension(element.getAttribute("size")); myScreenSize = getDimension(element.getAttribute("screenSize")); myScreenPos = getPoint(element.getAttribute("screenPos")); myCrop = getRectangle(element.getAttribute("crop")); myBackgroundName = getFileName(element, "back"); myShadowName = getFileName(element, "shadow"); myReflectionName = getFileName(element, "lights"); myMaskName = getFileName(element, "mask"); } @Nullable private static String getFileName(Element element, String name) { return name != null && !name.isEmpty() ? element.getAttribute(name) : null; } @Nullable private static Dimension getDimension(String value) { if (value == null || value.isEmpty()) { return null; } int comma = value.indexOf(','); if (comma == -1) { return null; } return new Dimension(getInteger(value.substring(0, comma)), getInteger(value.substring(comma + 1))); } @Nullable private static Point getPoint(String value) { if (value == null || value.isEmpty()) { return null; } int comma = value.indexOf(','); if (comma == -1) { return null; } return new Point(getInteger(value.substring(0, comma)), getInteger(value.substring(comma + 1))); } @Nullable private static Rectangle getRectangle(String value) { if (value == null || value.isEmpty()) { return null; } int comma1 = value.indexOf(','); if (comma1 == -1) { return null; } int comma2 = value.indexOf(',', comma1 + 1); if (comma2 == -1) { return null; } int comma3 = value.indexOf(',', comma2 + 1); if (comma3 == -1) { return null; } String x = value.substring(0, comma1); String y = value.substring(comma1 + 1, comma2); String w = value.substring(comma2 + 1, comma3); String h = value.substring(comma3 + 1); return new Rectangle(getInteger(x), getInteger(y), getInteger(w), getInteger(h)); } private static int getInteger(String value) { return Integer.parseInt(value); } public boolean isPortrait() { return myOrientation == ScreenOrientation.PORTRAIT; } public Dimension getScreenSize() { return myScreenSize; } public Point getScreenPos() { return myScreenPos; } public Dimension getFrameSize() { return myFrameSize; } public Rectangle getCrop() { return myCrop; } @NotNull public ScreenOrientation getOrientation() { return myOrientation; } @Nullable public File getBackgroundFile() { return !StringUtil.isEmpty(myBackgroundName) ? new File(myDevice.getBaseFolder(), myBackgroundName) : null; } @Nullable public File getShadowFile() { return !StringUtil.isEmpty(myShadowName) ? new File(myDevice.getBaseFolder(), myShadowName) : null; } @Nullable public File getReflectionFile() { return !StringUtil.isEmpty(myReflectionName) ? new File(myDevice.getBaseFolder(), myReflectionName) : null; } @Nullable public File getMaskFile() { return !StringUtil.isEmpty(myMaskName) ? new File(myDevice.getBaseFolder(), myMaskName) : null; } public double getAspectRatio() { return myScreenSize.width / (double) myScreenSize.height; } } }