diff options
author | Raphael Moll <ralf@android.com> | 2012-11-28 14:11:14 -0800 |
---|---|---|
committer | Raphael Moll <ralf@android.com> | 2012-11-29 00:41:17 -0800 |
commit | 0a78abcd5b998a10d9ae110a4a4e1f32ff2faf98 (patch) | |
tree | 5f7bba9b9a39496531cb4a3d3914a4e9a11f40a1 | |
parent | f654fe41fea466e2461bca58c04f2b0d5a3981f9 (diff) | |
download | sdk-0a78abcd5b998a10d9ae110a4a4e1f32ff2faf98.zip sdk-0a78abcd5b998a10d9ae110a4a4e1f32ff2faf98.tar.gz sdk-0a78abcd5b998a10d9ae110a4a4e1f32ff2faf98.tar.bz2 |
SDK: Relative AVD root path.
AVD have a root ini file in .android/avds/name.ini
with a "path" absolute path to the avd config directory.
This does 2 things:
- force the file to use ISO-8859-1 (aka Latin1) encoding.
- add a "path.rel" key with a relative path to the .android
folder if the avd directory is at its typical location.
The loading logic is:
- try to use the "path" key if it points to an existing
directory (as an absolute path).
- otherwise use the "path.rel" key as a path relative
to the .android folder if present.
SDK Bug: 40498
Change-Id: I2364c2ee69f48816f3e6f6f33c9fa43170e05587
3 files changed, 195 insertions, 10 deletions
diff --git a/common/src/com/android/prefs/AndroidLocation.java b/common/src/com/android/prefs/AndroidLocation.java index 66c0248..6af8e9b 100644 --- a/common/src/com/android/prefs/AndroidLocation.java +++ b/common/src/com/android/prefs/AndroidLocation.java @@ -24,6 +24,12 @@ import java.io.File; * Manages the location of the android files (including emulator files, ddms config, debug keystore) */ public final class AndroidLocation { + + /** + * The name of the .android folder returned by {@link #getFolder()}. + */ + public static final String FOLDER_DOT_ANDROID = ".android"; + /** * Virtual Device folder inside the path returned by {@link #getFolder()} */ @@ -57,7 +63,11 @@ public final class AndroidLocation { "Unable to get the Android SDK home directory.\n" + "Make sure the environment variable ANDROID_SDK_HOME is set up."); } else { - sPrefsLocation = home + File.separator + ".android" + File.separator; + sPrefsLocation = home; + if (!sPrefsLocation.endsWith(File.separator)) { + sPrefsLocation += File.separator; + } + sPrefsLocation += FOLDER_DOT_ANDROID + File.separator; } } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java index 5f35661..cd99388 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java @@ -17,8 +17,11 @@ package com.android.sdklib.internal.avd; import com.android.SdkConstants; +import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.io.FileWrapper; +import com.android.io.IAbstractFile; +import com.android.io.StreamException; import com.android.prefs.AndroidLocation; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.IAndroidTarget; @@ -33,7 +36,10 @@ import com.android.sdklib.util.GrabProcessOutput.IProcessOutput; import com.android.sdklib.util.GrabProcessOutput.Wait; import com.android.utils.ILogger; import com.android.utils.Pair; +import com.google.common.base.Charsets; +import com.google.common.io.Closeables; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -41,7 +47,9 @@ import java.io.FileOutputStream; import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; +import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -67,9 +75,30 @@ public class AvdManager { } } - public static final String AVD_FOLDER_EXTENSION = ".avd"; //$NON-NLS-1$ + private final static Pattern INI_LINE_PATTERN = + Pattern.compile("^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); //$NON-NLS-1$ - public static final String AVD_INFO_PATH = "path"; //$NON-NLS-1$ + public static final String AVD_FOLDER_EXTENSION = ".avd"; //$NON-NLS-1$ + + /** Charset encoding used by the avd.ini/config.ini. */ + public static final String AVD_INI_ENCODING = "avd.ini.encoding"; //$NON-NLS-1$ + + /** + * The *absolute* path to the AVD folder (which contains the #CONFIG_INI file). + */ + public static final String AVD_INFO_ABS_PATH = "path"; //$NON-NLS-1$ + + /** + * The path to the AVD folder (which contains the #CONFIG_INI file) relative to + * the {@link AndroidLocation#FOLDER_DOT_ANDROID}. This information is written + * in the avd ini <b>only</b> if the AVD folder is located under the .android path + * (that is the relative that has no backward {@code ..} references). + */ + public static final String AVD_INFO_REL_PATH = "path.rel"; //$NON-NLS-1$ + + /** + * The {@link IAndroidTarget#hashString()} of the AVD. + */ public static final String AVD_INFO_TARGET = "target"; //$NON-NLS-1$ /** @@ -1087,8 +1116,20 @@ public class AvdManager { } } + String absPath = avdFolder.getAbsolutePath(); + String relPath = null; + String androidPath = AndroidLocation.getFolder(); + if (absPath.startsWith(androidPath)) { + // Compute the AVD path relative to the android path. + assert androidPath.endsWith(File.separator); + relPath = absPath.substring(androidPath.length()); + } + HashMap<String, String> values = new HashMap<String, String>(); - values.put(AVD_INFO_PATH, avdFolder.getAbsolutePath()); + if (relPath != null) { + values.put(AVD_INFO_REL_PATH, relPath); + } + values.put(AVD_INFO_ABS_PATH, absPath); values.put(AVD_INFO_TARGET, target.hashString()); writeIniFile(iniFile, values); @@ -1339,13 +1380,27 @@ public class AvdManager { * valid or not. */ private AvdInfo parseAvdInfo(File iniPath, ILogger log) { - Map<String, String> map = ProjectProperties.parsePropertyFile( + Map<String, String> map = parseIniFile( new FileWrapper(iniPath), log); - String avdPath = map.get(AVD_INFO_PATH); + String avdPath = map.get(AVD_INFO_ABS_PATH); String targetHash = map.get(AVD_INFO_TARGET); + if (!(new File(avdPath).isDirectory())) { + // Try to fallback on the relative path, if present. + String relPath = map.get(AVD_INFO_REL_PATH); + if (relPath != null) { + try { + String androidPath = AndroidLocation.getFolder(); + File f = new File(androidPath, relPath); + if (f.isDirectory()) { + avdPath = f.getAbsolutePath(); + } + } catch (AndroidLocationException ignore) {} + } + } + IAndroidTarget target = null; FileWrapper configIniFile = null; Map<String, String> properties = null; @@ -1363,7 +1418,7 @@ public class AvdManager { if (!configIniFile.isFile()) { log.warning("Missing file '%1$s'.", configIniFile.getPath()); } else { - properties = ProjectProperties.parsePropertyFile(configIniFile, log); + properties = parseIniFile(configIniFile, log); } } @@ -1455,6 +1510,7 @@ public class AvdManager { /** * Writes a .ini file from a set of properties, using UTF-8 encoding. + * The file should be read back later by {@link #parseIniFile(IAbstractFile, ILogger)}. * * @param iniFile The file to generate. * @param values THe properties to place in the ini file. @@ -1462,8 +1518,12 @@ public class AvdManager { */ private static void writeIniFile(File iniFile, Map<String, String> values) throws IOException { - OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(iniFile), - SdkConstants.INI_CHARSET); + + Charset charset = Charsets.ISO_8859_1; + OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(iniFile), charset); + + // Write down the charset used in case we want to use it later. + writer.write(String.format("%1$s=%2$s\n", AVD_INI_ENCODING, charset.name())); for (Entry<String, String> entry : values.entrySet()) { writer.write(String.format("%1$s=%2$s\n", entry.getKey(), entry.getValue())); @@ -1472,6 +1532,108 @@ public class AvdManager { } /** + * Parses a property file and returns a map of the content. + * <p/> + * If the file is not present, null is returned with no error messages sent to the log. + * <p/> + * Charset encoding will be either the system's default or the one specified by the + * {@link #AVD_INI_ENCODING} key if present. + * + * @param propFile the property file to parse + * @param log the ILogger object receiving warning/error from the parsing. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + private static Map<String, String> parseIniFile( + @NonNull IAbstractFile propFile, + @Nullable ILogger log) { + return parseIniFileImpl(propFile, log, null /*charset*/); + } + + /** + * Implementation helper for the {@link #parseIniFile(IAbstractFile, ILogger)} method. + * Don't call this one directly. + * + * @param propFile the property file to parse + * @param log the ILogger object receiving warning/error from the parsing. + * @param charset When a specific charset is specified, this will be used as-is. + * When null, the default charset will first be used and if the key + * {@link #AVD_INI_ENCODING} is found the parsing will restart using that specific + * charset. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + private static Map<String, String> parseIniFileImpl( + @NonNull IAbstractFile propFile, + @Nullable ILogger log, + @NonNull Charset charset) { + + BufferedReader reader = null; + try { + boolean canChangeCharset = false; + if (charset == null) { + canChangeCharset = false; + charset = Charsets.ISO_8859_1; + } + reader = new BufferedReader(new InputStreamReader(propFile.getContents(), charset)); + + String line = null; + Map<String, String> map = new HashMap<String, String>(); + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() > 0 && line.charAt(0) != '#') { + + Matcher m = INI_LINE_PATTERN.matcher(line); + if (m.matches()) { + // Note: we do NOT escape values. + String key = m.group(1); + String value = m.group(2); + + // If we find the charset encoding and it's not the same one and + // it's a valid one, re-read the file using that charset. + if (canChangeCharset && + AVD_INI_ENCODING.equals(key) && + !charset.name().equals(value) && + Charset.isSupported(value)) { + charset = Charset.forName(value); + return parseIniFileImpl(propFile, log, charset); + } + + map.put(key, value); + } else { + if (log != null) { + log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", + propFile.getOsLocation(), + line); + } + return null; + } + } + } + + return map; + } catch (FileNotFoundException e) { + // this should not happen since we usually test the file existence before + // calling the method. + // Return null below. + } catch (IOException e) { + if (log != null) { + log.warning("Error parsing '%1$s': %2$s.", + propFile.getOsLocation(), + e.getMessage()); + } + } catch (StreamException e) { + if (log != null) { + log.warning("Error parsing '%1$s': %2$s.", + propFile.getOsLocation(), + e.getMessage()); + } + } finally { + Closeables.closeQuietly(reader); + } + + return null; + } + + /** * Invokes the tool to create a new SD card image file. * * @param toolLocation The path to the mksdcard tool. diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java index ef931ee..d2a5fdb 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java @@ -36,6 +36,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; +import java.util.Properties; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -425,7 +426,19 @@ public class ProjectProperties implements IPropertySource { /** * Parses a property file (using UTF-8 encoding) and returns a map of the content. - * <p/>If the file is not present, null is returned with no error messages sent to the log. + * <p/> + * If the file is not present, null is returned with no error messages sent to the log. + * <p/> + * IMPORTANT: This method is now unfortunately used in multiple places to parse random + * property files. This is NOT a safe practice since there is no corresponding method + * to write property files unless you use {@link ProjectPropertiesWorkingCopy#save()}. + * Code that writes INI or properties without at least using {@link #escape(String)} will + * certainly not load back correct data. <br/> + * Unless there's a strong legacy need to support existing files, new callers should + * probably just use Java's {@link Properties} which has well defined semantics. + * It's also a mistake to write/read property files using this code and expect it to + * work with Java's {@link Properties} or external tools (e.g. ant) since there can be + * differences in escaping and in character encoding. * * @param propFile the property file to parse * @param log the ILogger object receiving warning/error from the parsing. |