/* * Copyright (C) 2012 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.sdklib.devices; import static com.android.SdkConstants.VALUE_FALSE; import static com.android.SdkConstants.VALUE_TRUE; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.dvlib.DeviceSchema; import com.android.resources.Density; import com.android.resources.Keyboard; import com.android.resources.KeyboardState; import com.android.resources.Navigation; import com.android.resources.NavigationState; import com.android.resources.ScreenOrientation; import com.android.resources.ScreenRatio; import com.android.resources.ScreenRound; import com.android.resources.ScreenSize; import com.android.resources.TouchScreen; import com.android.resources.UiMode; import com.google.common.base.Splitter; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import java.awt.Point; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.validation.Schema; public class DeviceParser { private static class DeviceHandler extends DefaultHandler { private static final Splitter sSpaceSplitter = Splitter.on(' ').omitEmptyStrings(); private static final String ROUND_BOOT_PROP = "ro.emulator.circular"; private static final String CHIN_BOOT_PROP = "ro.emu.win_outset_bottom_px"; private final Table<String, String, Device> mDevices = HashBasedTable.create(); private final StringBuilder mStringAccumulator = new StringBuilder(); private final File mParentFolder; private Meta mMeta; private Hardware mHardware; private Software mSoftware; private State mState; private Device.Builder mBuilder; private Camera mCamera; private Storage.Unit mUnit; private String[] mBootProp; public DeviceHandler(@Nullable File parentFolder) { mParentFolder = parentFolder; } @NonNull public Table<String, String, Device> getDevices() { return mDevices; } @Override public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { if (DeviceSchema.NODE_DEVICE.equals(localName)) { // Reset everything mMeta = null; mHardware = null; mSoftware = null; mState = null; mCamera = null; mBuilder = new Device.Builder(); } else if (DeviceSchema.NODE_META.equals(localName)) { mMeta = new Meta(); } else if (DeviceSchema.NODE_HARDWARE.equals(localName)) { mHardware = new Hardware(); } else if (DeviceSchema.NODE_SOFTWARE.equals(localName)) { mSoftware = new Software(); } else if (DeviceSchema.NODE_STATE.equals(localName)) { mState = new State(); // mState can embed a Hardware instance mHardware = mHardware.deepCopy(); String defaultState = attributes.getValue(DeviceSchema.ATTR_DEFAULT); if ("true".equals(defaultState) || "1".equals(defaultState)) { mState.setDefaultState(true); } mState.setName(attributes.getValue(DeviceSchema.ATTR_NAME).trim()); } else if (DeviceSchema.NODE_CAMERA.equals(localName)) { mCamera = new Camera(); } else if (DeviceSchema.NODE_RAM.equals(localName) || DeviceSchema.NODE_INTERNAL_STORAGE.equals(localName) || DeviceSchema.NODE_REMOVABLE_STORAGE.equals(localName)) { mUnit = Storage.Unit.getEnum(attributes.getValue(DeviceSchema.ATTR_UNIT)); } else if (DeviceSchema.NODE_FRAME.equals(localName)) { mMeta.setFrameOffsetLandscape(new Point()); mMeta.setFrameOffsetPortrait(new Point()); } else if (DeviceSchema.NODE_SCREEN.equals(localName)) { mHardware.setScreen(new Screen()); } else if (DeviceSchema.NODE_BOOT_PROP.equals(localName)) { mBootProp = new String[2]; } mStringAccumulator.setLength(0); } @Override public void characters(char[] ch, int start, int length) { mStringAccumulator.append(ch, start, length); } @Override public void endElement(String uri, String localName, String name) throws SAXException { if (DeviceSchema.NODE_DEVICE.equals(localName)) { Device device = mBuilder.build(); mDevices.put(device.getId(), device.getManufacturer(), device); } else if (DeviceSchema.NODE_NAME.equals(localName)) { mBuilder.setName(getString(mStringAccumulator)); } else if (DeviceSchema.NODE_ID.equals(localName)) { mBuilder.setId(getString(mStringAccumulator)); } else if (DeviceSchema.NODE_MANUFACTURER.equals(localName)) { mBuilder.setManufacturer(getString(mStringAccumulator)); } else if (DeviceSchema.NODE_META.equals(localName)) { mBuilder.setMeta(mMeta); } else if (DeviceSchema.NODE_SOFTWARE.equals(localName)) { mBuilder.addSoftware(mSoftware); } else if (DeviceSchema.NODE_STATE.equals(localName)) { mState.setHardware(mHardware); mBuilder.addState(mState); } else if (DeviceSchema.NODE_SIXTY_FOUR.equals(localName)) { mMeta.setIconSixtyFour(new File(mParentFolder, getString(mStringAccumulator))); } else if (DeviceSchema.NODE_SIXTEEN.equals(localName)) { mMeta.setIconSixteen(new File(mParentFolder, getString(mStringAccumulator))); } else if (DeviceSchema.NODE_PATH.equals(localName)) { mMeta.setFrame(new File(mParentFolder, mStringAccumulator.toString().trim())); } else if (DeviceSchema.NODE_PORTRAIT_X_OFFSET.equals(localName)) { mMeta.getFrameOffsetPortrait().x = getInteger(mStringAccumulator); } else if (DeviceSchema.NODE_PORTRAIT_Y_OFFSET.equals(localName)) { mMeta.getFrameOffsetPortrait().y = getInteger(mStringAccumulator); } else if (DeviceSchema.NODE_LANDSCAPE_X_OFFSET.equals(localName)) { mMeta.getFrameOffsetLandscape().x = getInteger(mStringAccumulator); } else if (DeviceSchema.NODE_LANDSCAPE_Y_OFFSET.equals(localName)) { mMeta.getFrameOffsetLandscape().y = getInteger(mStringAccumulator); } else if (DeviceSchema.NODE_SCREEN_SIZE.equals(localName)) { mHardware.getScreen().setSize(ScreenSize.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_DIAGONAL_LENGTH.equals(localName)) { mHardware.getScreen().setDiagonalLength(getDouble(mStringAccumulator)); } else if (DeviceSchema.NODE_PIXEL_DENSITY.equals(localName)) { mHardware.getScreen().setPixelDensity( Density.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_SCREEN_RATIO.equals(localName)) { mHardware.getScreen().setRatio( ScreenRatio.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_X_DIMENSION.equals(localName)) { mHardware.getScreen().setXDimension(getInteger(mStringAccumulator)); } else if (DeviceSchema.NODE_Y_DIMENSION.equals(localName)) { mHardware.getScreen().setYDimension(getInteger(mStringAccumulator)); } else if (DeviceSchema.NODE_XDPI.equals(localName)) { mHardware.getScreen().setXdpi(getDouble(mStringAccumulator)); } else if (DeviceSchema.NODE_YDPI.equals(localName)) { mHardware.getScreen().setYdpi(getDouble(mStringAccumulator)); } else if (DeviceSchema.NODE_MULTITOUCH.equals(localName)) { mHardware.getScreen().setMultitouch( Multitouch.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_MECHANISM.equals(localName)) { mHardware.getScreen().setMechanism( TouchScreen.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_SCREEN_TYPE.equals(localName)) { mHardware.getScreen().setScreenType( ScreenType.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_NETWORKING.equals(localName)) { for (String n : getStringList(mStringAccumulator)) { Network net = Network.getEnum(n); if (net != null) { mHardware.addNetwork(net); } } } else if (DeviceSchema.NODE_SENSORS.equals(localName)) { for (String s : getStringList(mStringAccumulator)) { Sensor sens = Sensor.getEnum(s); if (sens != null) { mHardware.addSensor(sens); } } } else if (DeviceSchema.NODE_MIC.equals(localName)) { mHardware.setHasMic(getBool(mStringAccumulator)); } else if (DeviceSchema.NODE_CAMERA.equals(localName)) { mHardware.addCamera(mCamera); mCamera = null; } else if (DeviceSchema.NODE_LOCATION.equals(localName)) { CameraLocation location = CameraLocation.getEnum(getString(mStringAccumulator)); if (location != null) { mCamera.setLocation(location); } } else if (DeviceSchema.NODE_AUTOFOCUS.equals(localName)) { mCamera.setFlash(getBool(mStringAccumulator)); } else if (DeviceSchema.NODE_FLASH.equals(localName)) { mCamera.setFlash(getBool(mStringAccumulator)); } else if (DeviceSchema.NODE_KEYBOARD.equals(localName)) { mHardware.setKeyboard(Keyboard.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_NAV.equals(localName)) { mHardware.setNav(Navigation.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_RAM.equals(localName)) { int val = getInteger(mStringAccumulator); mHardware.setRam(new Storage(val, mUnit)); } else if (DeviceSchema.NODE_BUTTONS.equals(localName)) { ButtonType buttonType = ButtonType.getEnum(getString(mStringAccumulator)); if (buttonType != null) { mHardware.setButtonType(buttonType); } } else if (DeviceSchema.NODE_INTERNAL_STORAGE.equals(localName)) { for (String s : getStringList(mStringAccumulator)) { int val = Integer.parseInt(s); mHardware.addInternalStorage(new Storage(val, mUnit)); } } else if (DeviceSchema.NODE_REMOVABLE_STORAGE.equals(localName)) { for (String s : getStringList(mStringAccumulator)) { if (s != null && !s.isEmpty()) { int val = Integer.parseInt(s); mHardware.addRemovableStorage(new Storage(val, mUnit)); } } } else if (DeviceSchema.NODE_CPU.equals(localName)) { mHardware.setCpu(getString(mStringAccumulator)); } else if (DeviceSchema.NODE_GPU.equals(localName)) { mHardware.setGpu(getString(mStringAccumulator)); } else if (DeviceSchema.NODE_ABI.equals(localName)) { for (String s : getStringList(mStringAccumulator)) { Abi abi = Abi.getEnum(s); if (abi != null) { mHardware.addSupportedAbi(abi); } } } else if (DeviceSchema.NODE_DOCK.equals(localName)) { for (String s : getStringList(mStringAccumulator)) { UiMode d = UiMode.getEnum(s); if (d != null) { mHardware.addSupportedUiMode(d); } } } else if (DeviceSchema.NODE_POWER_TYPE.equals(localName)) { PowerType type = PowerType.getEnum(getString(mStringAccumulator)); if (type != null) { mHardware.setChargeType(type); } } else if (DeviceSchema.NODE_API_LEVEL.equals(localName)) { String val = getString(mStringAccumulator); // Can be one of 5 forms: // 1 // 1-2 // 1- // -2 // - int index; if (val.charAt(0) == '-') { if (val.length() == 1) { // - mSoftware.setMinSdkLevel(0); mSoftware.setMaxSdkLevel(Integer.MAX_VALUE); } else { // -2 // Remove the front dash and any whitespace between it // and the upper bound. val = val.substring(1).trim(); mSoftware.setMinSdkLevel(0); mSoftware.setMaxSdkLevel(Integer.parseInt(val)); } } else if ((index = val.indexOf('-')) > 0) { if (index == val.length() - 1) { // 1- // Strip the last dash and any whitespace between it and // the lower bound. val = val.substring(0, val.length() - 1).trim(); mSoftware.setMinSdkLevel(Integer.parseInt(val)); mSoftware.setMaxSdkLevel(Integer.MAX_VALUE); } else { // 1-2 String min = val.substring(0, index).trim(); String max = val.substring(index + 1); mSoftware.setMinSdkLevel(Integer.parseInt(min)); mSoftware.setMaxSdkLevel(Integer.parseInt(max)); } } else { // 1 int apiLevel = Integer.parseInt(val); mSoftware.setMinSdkLevel(apiLevel); mSoftware.setMaxSdkLevel(apiLevel); } } else if (DeviceSchema.NODE_LIVE_WALLPAPER_SUPPORT.equals(localName)) { mSoftware.setLiveWallpaperSupport(getBool(mStringAccumulator)); } else if (DeviceSchema.NODE_BLUETOOTH_PROFILES.equals(localName)) { for (String s : getStringList(mStringAccumulator)) { BluetoothProfile profile = BluetoothProfile.getEnum(s); if (profile != null) { mSoftware.addBluetoothProfile(profile); } } } else if (DeviceSchema.NODE_GL_VERSION.equals(localName)) { // Guaranteed to be in the form [\d]\.[\d] mSoftware.setGlVersion(getString(mStringAccumulator)); } else if (DeviceSchema.NODE_GL_EXTENSIONS.equals(localName)) { mSoftware.addAllGlExtensions(getStringList(mStringAccumulator)); } else if (DeviceSchema.NODE_DESCRIPTION.equals(localName)) { mState.setDescription(getString(mStringAccumulator)); } else if (DeviceSchema.NODE_SCREEN_ORIENTATION.equals(localName)) { mState.setOrientation(ScreenOrientation.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_KEYBOARD_STATE.equals(localName)) { mState.setKeyState(KeyboardState.getEnum(getString(mStringAccumulator))); } else if (DeviceSchema.NODE_NAV_STATE.equals(localName)) { // We have an extra state in our XML for nonav that // NavigationState doesn't contain String navState = getString(mStringAccumulator); if (navState.equals("nonav")) { mState.setNavState(NavigationState.HIDDEN); } else { mState.setNavState(NavigationState.getEnum(getString(mStringAccumulator))); } } else if (DeviceSchema.NODE_STATUS_BAR.equals(localName)) { mSoftware.setStatusBar(getBool(mStringAccumulator)); } else if (DeviceSchema.NODE_TAG_ID.equals(localName)) { mBuilder.setTagId(getString(mStringAccumulator)); } else if (DeviceSchema.NODE_PROP_NAME.equals(localName)) { assert mBootProp != null && mBootProp.length == 2; mBootProp[0] = getString(mStringAccumulator); } else if (DeviceSchema.NODE_PROP_VALUE.equals(localName)) { assert mBootProp != null && mBootProp.length == 2; mBootProp[1] = mStringAccumulator.toString(); } else if (DeviceSchema.NODE_BOOT_PROP.equals(localName)) { assert mBootProp != null && mBootProp.length == 2 && mBootProp[0] != null && mBootProp[1] != null; mBuilder.addBootProp(mBootProp[0], mBootProp[1]); checkAndSetIfRound(mBootProp[0], mBootProp[1]); mBootProp = null; } else if (DeviceSchema.NODE_SKIN.equals(localName)) { String path = getString(mStringAccumulator).replace('/', File.separatorChar); mHardware.setSkinFile(new File(path)); } } @Override public void error(SAXParseException e) throws SAXParseException { throw e; } private void checkAndSetIfRound(String bootPropKey, String bootPropValue) { // This is a ugly hack. To keep the existing devices.xmls working, the roundness of the // screen is stored in a boot property. ScreenRound roundness = null; if (ROUND_BOOT_PROP.equals(bootPropKey)) { if (VALUE_TRUE.equals(bootPropValue)) { roundness = ScreenRound.ROUND; } else if (VALUE_FALSE.equals(bootPropValue)) { roundness = ScreenRound.NOTROUND; } for (State state : mBuilder.getAllStates()) { state.getHardware().getScreen().setScreenRound(roundness); } } if (CHIN_BOOT_PROP.equals(bootPropKey)) { int chin = Integer.parseInt(bootPropValue); for (State state : mBuilder.getAllStates()) { state.getHardware().getScreen().setChin(chin); } } } private static List<String> getStringList(StringBuilder stringAccumulator) { List<String> filteredStrings = new ArrayList<String>(); for (String s : sSpaceSplitter.split(stringAccumulator)) { if (s != null && !s.isEmpty()) { filteredStrings.add(s.trim()); } } return filteredStrings; } private static Boolean getBool(StringBuilder s) { return equals(s, "true") || equals(s, "1"); } private static double getDouble(StringBuilder stringAccumulator) { return Double.parseDouble(getString(stringAccumulator)); } private static String getString(StringBuilder s) { return s.toString().trim(); } private static boolean equals(StringBuilder s, String t) { int start = 0; int length = s.length(); while (start < length && Character.isWhitespace(s.charAt(start))) { start++; } if (start == length) { return t.isEmpty(); } int end = length; while (end > start && Character.isWhitespace(s.charAt(end - 1))) { end--; } if (t.length() != (end - start)) { return false; } for (int i = 0, n = t.length(), j = start; i < n; i++, j++) { if (Character.toLowerCase(s.charAt(j)) != Character.toLowerCase(t.charAt(i))) { return false; } } return true; } private static int getInteger(StringBuilder stringAccumulator) { return Integer.parseInt(getString(stringAccumulator)); } } private static final SAXParserFactory sParserFactory; static { sParserFactory = SAXParserFactory.newInstance(); sParserFactory.setNamespaceAware(true); } @NonNull public static Table<String, String, Device> parse(@NonNull File devicesFile) throws SAXException, ParserConfigurationException, IOException { // stream closed by parseImpl. @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") InputStream stream = new FileInputStream(devicesFile); return parseImpl(stream, devicesFile.getAbsoluteFile().getParentFile()); } /** * This method closes the stream. */ @NonNull public static Table<String, String, Device> parse(@NonNull InputStream devices) throws SAXException, IOException, ParserConfigurationException { return parseImpl(devices, null); } /** * After parsing, this method closes the stream. */ @NonNull private static Table<String, String, Device> parseImpl(@NonNull InputStream devices, @Nullable File parentDir) throws SAXException, IOException, ParserConfigurationException { try { if (!devices.markSupported()) { //noinspection IOResourceOpenedButNotSafelyClosed devices = new BufferedInputStream(devices); // closed in the finally block. } devices.mark(500000); int version = DeviceSchema.getXmlSchemaVersion(devices); SAXParser parser = getParser(version); DeviceHandler dHandler = new DeviceHandler(parentDir); devices.reset(); parser.parse(devices, dHandler); return dHandler.getDevices(); } finally { // It's better to close the stream here since we may have created it above. devices.close(); } } @NonNull private static SAXParser getParser(int version) throws ParserConfigurationException, SAXException { Schema schema = DeviceSchema.getSchema(version); if (schema != null) { sParserFactory.setSchema(schema); } return sParserFactory.newSAXParser(); } }