diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
commit | 55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch) | |
tree | ecd18b995aea8eeeb8b3823266280d41245bf0f7 /sdkmanager | |
parent | 82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff) | |
download | sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'sdkmanager')
41 files changed, 7342 insertions, 0 deletions
diff --git a/sdkmanager/Android.mk b/sdkmanager/Android.mk new file mode 100644 index 0000000..30df7f1 --- /dev/null +++ b/sdkmanager/Android.mk @@ -0,0 +1,18 @@ +# +# Copyright (C) 2008 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. +# +SDKMANAGER_LOCAL_DIR := $(call my-dir) +include $(SDKMANAGER_LOCAL_DIR)/app/Android.mk +include $(SDKMANAGER_LOCAL_DIR)/libs/Android.mk diff --git a/sdkmanager/MODULE_LICENSE_APACHE2 b/sdkmanager/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/sdkmanager/MODULE_LICENSE_APACHE2 diff --git a/sdkmanager/app/.classpath b/sdkmanager/app/.classpath new file mode 100644 index 0000000..cbd9d37 --- /dev/null +++ b/sdkmanager/app/.classpath @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="src" path="tests"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/> + <classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/> + <classpathentry combineaccessrules="false" kind="src" path="/SdkUiLib"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/sdkmanager/app/.project b/sdkmanager/app/.project new file mode 100644 index 0000000..e12c17d --- /dev/null +++ b/sdkmanager/app/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>SdkManager</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/sdkmanager/app/Android.mk b/sdkmanager/app/Android.mk new file mode 100644 index 0000000..24ba61f --- /dev/null +++ b/sdkmanager/app/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2007 The Android Open Source Project +# +SDKMANAGERAPP_LOCAL_DIR := $(call my-dir) +include $(SDKMANAGERAPP_LOCAL_DIR)/etc/Android.mk +include $(SDKMANAGERAPP_LOCAL_DIR)/src/Android.mk diff --git a/sdkmanager/app/etc/Android.mk b/sdkmanager/app/etc/Android.mk new file mode 100644 index 0000000..8723cd8 --- /dev/null +++ b/sdkmanager/app/etc/Android.mk @@ -0,0 +1,8 @@ +# Copyright 2008 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PREBUILT_EXECUTABLES := android +include $(BUILD_HOST_PREBUILT) + diff --git a/sdkmanager/app/etc/android b/sdkmanager/app/etc/android new file mode 100755 index 0000000..af4042b --- /dev/null +++ b/sdkmanager/app/etc/android @@ -0,0 +1,84 @@ +#!/bin/sh +# Copyright 2005-2007, 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. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=sdkmanager.jar +frameworkdir="$progdir" +libdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + + +# Check args. +if [ debug = "$1" ]; then + # add this in for debugging + java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +else + java_debug= +fi + +# Mac OS X needs an additional arg, or you get an "illegal thread" complaint. +if [ `uname` = "Darwin" ]; then + os_opts="-XstartOnFirstThread" + #because Java 1.6 is 64 bits only and SWT doesn't support this, we force the usage of java 1.5 + java_cmd="/System/Library/Frameworks/JavaVM.framework/Versions/1.5/Commands/java" +else + os_opts= + java_cmd="java" +fi + +if [ "$OSTYPE" = "cygwin" ] ; then + jarpath=`cygpath -w "$frameworkdir/$jarfile"` + progdir=`cygpath -w "$progdir"` +else + jarpath="$frameworkdir/$jarfile" +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +# might need more memory, e.g. -Xmx128M +exec "$java_cmd" -Xmx256M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Djava.library.path="$libdir" -Dcom.android.sdkmanager.toolsdir="$progdir" -jar "$jarpath" "$@" diff --git a/sdkmanager/app/etc/android.bat b/sdkmanager/app/etc/android.bat new file mode 100755 index 0000000..de950ed --- /dev/null +++ b/sdkmanager/app/etc/android.bat @@ -0,0 +1,51 @@ +@echo off +rem Copyright (C) 2007 The Android Open Source Project +rem +rem Licensed under the Apache License, Version 2.0 (the "License"); +rem you may not use this file except in compliance with the License. +rem You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem don't modify the caller's environment +setlocal + +rem Set up prog to be the path of this script, including following symlinks, +rem and set up progdir to be the fully-qualified pathname of its directory. +set prog=%~f0 + +rem Grab current directory before we change it +set workdir=%cd% + +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 + +set jarfile=sdkmanager.jar +set frameworkdir= +set libdir= + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=lib\ + set libdir=lib\ + +if exist %frameworkdir%%jarfile% goto JarFileOk + set frameworkdir=..\framework\ + set libdir=..\lib\ + +:JarFileOk + +if debug NEQ "%1" goto NoDebug + set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +:NoDebug + +set jarpath=%frameworkdir%%jarfile% + +call java %java_debug% -Djava.ext.dirs=%frameworkdir% -Djava.library.path=%libdir% -Dcom.android.sdkmanager.toolsdir= -Dcom.android.sdkmanager.workdir="%workdir%" -jar %jarpath% %* diff --git a/sdkmanager/app/etc/manifest.txt b/sdkmanager/app/etc/manifest.txt new file mode 100644 index 0000000..5676634 --- /dev/null +++ b/sdkmanager/app/etc/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.sdkmanager.Main diff --git a/sdkmanager/app/src/Android.mk b/sdkmanager/app/src/Android.mk new file mode 100644 index 0000000..b508076 --- /dev/null +++ b/sdkmanager/app/src/Android.mk @@ -0,0 +1,16 @@ +# Copyright 2007 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + androidprefs \ + sdklib \ + sdkuilib +LOCAL_MODULE := sdkmanager + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/sdkmanager/app/src/com/android/sdkmanager/CommandLineProcessor.java b/sdkmanager/app/src/com/android/sdkmanager/CommandLineProcessor.java new file mode 100644 index 0000000..9f3fb99 --- /dev/null +++ b/sdkmanager/app/src/com/android/sdkmanager/CommandLineProcessor.java @@ -0,0 +1,791 @@ +/* + * Copyright (C) 2008 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.sdkmanager; + +import com.android.sdklib.ISdkLog; + +import java.util.HashMap; +import java.util.Map.Entry; + +/** + * Parses the command-line and stores flags needed or requested. + * <p/> + * This is a base class. To be useful you want to: + * <ul> + * <li>override it. + * <li>pass an action array to the constructor. + * <li>define flags for your actions. + * </ul> + * <p/> + * To use, call {@link #parseArgs(String[])} and then + * call {@link #getValue(String, String, String)}. + */ +public class CommandLineProcessor { + + /** Internal verb name for internally hidden flags. */ + public final static String GLOBAL_FLAG_VERB = "@@internal@@"; + + /** String to use when the verb doesn't need any object. */ + public final static String NO_VERB_OBJECT = ""; + + /** The global help flag. */ + public static final String KEY_HELP = "help"; + /** The global verbose flag. */ + public static final String KEY_VERBOSE = "verbose"; + /** The global silent flag. */ + public static final String KEY_SILENT = "silent"; + + /** Verb requested by the user. Null if none specified, which will be an error. */ + private String mVerbRequested; + /** Direct object requested by the user. Can be null. */ + private String mDirectObjectRequested; + + /** + * Action definitions. + * <p/> + * Each entry is a string array with: + * <ul> + * <li> the verb. + * <li> a direct object (use #NO_VERB_OBJECT if there's no object). + * <li> a description. + * <li> an alternate form for the object (e.g. plural). + * </ul> + */ + private final String[][] mActions; + + private static final int ACTION_VERB_INDEX = 0; + private static final int ACTION_OBJECT_INDEX = 1; + private static final int ACTION_DESC_INDEX = 2; + private static final int ACTION_ALT_OBJECT_INDEX = 3; + + /** + * The map of all defined arguments. + * <p/> + * The key is a string "verb/directObject/longName". + */ + private final HashMap<String, Arg> mArguments = new HashMap<String, Arg>(); + /** Logger */ + private final ISdkLog mLog; + + public CommandLineProcessor(ISdkLog logger, String[][] actions) { + mLog = logger; + mActions = actions; + + define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "v", KEY_VERBOSE, + "Verbose mode: errors, warnings and informational messages are printed.", + false); + define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "s", KEY_SILENT, + "Silent mode: only errors are printed out.", + false); + define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "h", KEY_HELP, + "This help.", + false); + } + + //------------------ + // Helpers to get flags values + + /** Helper that returns true if --verbose was requested. */ + public boolean isVerbose() { + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_VERBOSE)).booleanValue(); + } + + /** Helper that returns true if --silent was requested. */ + public boolean isSilent() { + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_SILENT)).booleanValue(); + } + + /** Helper that returns true if --help was requested. */ + public boolean isHelpRequested() { + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_HELP)).booleanValue(); + } + + /** Returns the verb name from the command-line. Can be null. */ + public String getVerb() { + return mVerbRequested; + } + + /** Returns the direct object name from the command-line. Can be null. */ + public String getDirectObject() { + return mDirectObjectRequested; + } + + //------------------ + + /** + * Raw access to parsed parameter values. + * <p/> + * The default is to scan all parameters. Parameters that have been explicitly set on the + * command line are returned first. Otherwise one with a non-null value is returned. + * <p/> + * Both a verb and a direct object filter can be specified. When they are non-null they limit + * the scope of the search. + * <p/> + * If nothing has been found, return the last default value seen matching the filter. + * + * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. If null, all possible + * verbs that match the direct object condition will be examined and the first + * value set will be used. + * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. If null, + * all possible direct objects that match the verb condition will be examined and + * the first value set will be used. + * @param longFlagName The long flag name for the given action. Mandatory. Cannot be null. + * @return The current value object stored in the parameter, which depends on the argument mode. + */ + public Object getValue(String verb, String directObject, String longFlagName) { + + if (verb != null && directObject != null) { + String key = verb + "/" + directObject + "/" + longFlagName; + Arg arg = mArguments.get(key); + return arg.getCurrentValue(); + } + + Object lastDefault = null; + for (Arg arg : mArguments.values()) { + if (arg.getLongArg().equals(longFlagName)) { + if (verb == null || arg.getVerb().equals(verb)) { + if (directObject == null || arg.getDirectObject().equals(directObject)) { + if (arg.isInCommandLine()) { + return arg.getCurrentValue(); + } + if (arg.getCurrentValue() != null) { + lastDefault = arg.getCurrentValue(); + } + } + } + } + } + + return lastDefault; + } + + /** + * Internal setter for raw parameter value. + * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. + * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. + * @param longFlagName The long flag name for the given action. + * @param value The new current value object stored in the parameter, which depends on the + * argument mode. + */ + protected void setValue(String verb, String directObject, String longFlagName, Object value) { + String key = verb + "/" + directObject + "/" + longFlagName; + Arg arg = mArguments.get(key); + arg.setCurrentValue(value); + } + + /** + * Parses the command-line arguments. + * <p/> + * This method will exit and not return if a parsing error arise. + * + * @param args The arguments typically received by a main method. + */ + public void parseArgs(String[] args) { + String needsHelp = null; + String verb = null; + String directObject = null; + + try { + int n = args.length; + for (int i = 0; i < n; i++) { + Arg arg = null; + String a = args[i]; + if (a.startsWith("--")) { + arg = findLongArg(verb, directObject, a.substring(2)); + } else if (a.startsWith("-")) { + arg = findShortArg(verb, directObject, a.substring(1)); + } + + // No matching argument name found + if (arg == null) { + // Does it looks like a dashed parameter? + if (a.startsWith("-")) { + if (verb == null || directObject == null) { + // It looks like a dashed parameter and we don't have a a verb/object + // set yet, the parameter was just given too early. + + needsHelp = String.format( + "Flag '%1$s' is not a valid global flag. Did you mean to specify it after the verb/object name?", + a); + return; + } else { + // It looks like a dashed parameter and but it is unknown by this + // verb-object combination + + needsHelp = String.format( + "Flag '%1$s' is not valid for '%2$s %3$s'.", + a, verb, directObject); + return; + } + } + + if (verb == null) { + // Fill verb first. Find it. + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(a)) { + verb = a; + break; + } + } + + // Error if it was not a valid verb + if (verb == null) { + needsHelp = String.format( + "Expected verb after global parameters but found '%1$s' instead.", + a); + return; + } + + } else if (directObject == null) { + // Then fill the direct object. Find it. + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(verb)) { + if (actionDesc[ACTION_OBJECT_INDEX].equals(a)) { + directObject = a; + break; + } else if (actionDesc.length > ACTION_ALT_OBJECT_INDEX && + actionDesc[ACTION_ALT_OBJECT_INDEX].equals(a)) { + // if the alternate form exist and is used, we internally + // only memorize the default direct object form. + directObject = actionDesc[ACTION_OBJECT_INDEX]; + break; + } + } + } + + // Error if it was not a valid object for that verb + if (directObject == null) { + needsHelp = String.format( + "Expected verb after global parameters but found '%1$s' instead.", + a); + return; + + } + } + } else if (arg != null) { + // This argument was present on the command line + arg.setInCommandLine(true); + + // Process keyword + String error = null; + if (arg.getMode().needsExtra()) { + if (++i >= n) { + needsHelp = String.format("Missing argument for flag %1$s.", a); + return; + } + + error = arg.getMode().process(arg, args[i]); + } else { + error = arg.getMode().process(arg, null); + + // If we just toggled help, we want to exit now without printing any error. + // We do this test here only when a Boolean flag is toggled since booleans + // are the only flags that don't take parameters and help is a boolean. + if (isHelpRequested()) { + printHelpAndExit(null); + // The call above should terminate however in unit tests we override + // it so we still need to return here. + return; + } + } + + if (error != null) { + needsHelp = String.format("Invalid usage for flag %1$s: %2$s.", a, error); + return; + } + } + } + + if (needsHelp == null) { + if (verb == null) { + needsHelp = "Missing verb name."; + } else { + if (directObject == null) { + // Make sure this verb has an optional direct object + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(verb) && + actionDesc[ACTION_OBJECT_INDEX].equals(NO_VERB_OBJECT)) { + directObject = NO_VERB_OBJECT; + break; + } + } + + if (directObject == null) { + needsHelp = String.format("Missing object name for verb '%1$s'.", verb); + return; + } + } + + // Validate that all mandatory arguments are non-null for this action + String missing = null; + boolean plural = false; + for (Entry<String, Arg> entry : mArguments.entrySet()) { + Arg arg = entry.getValue(); + if (arg.getVerb().equals(verb) && + arg.getDirectObject().equals(directObject)) { + if (arg.isMandatory() && arg.getCurrentValue() == null) { + if (missing == null) { + missing = "--" + arg.getLongArg(); + } else { + missing += ", --" + arg.getLongArg(); + plural = true; + } + } + } + } + + if (missing != null) { + needsHelp = String.format( + "The %1$s %2$s must be defined for action '%3$s %4$s'", + plural ? "parameters" : "parameter", + missing, + verb, + directObject); + } + + mVerbRequested = verb; + mDirectObjectRequested = directObject; + } + } + } finally { + if (needsHelp != null) { + printHelpAndExitForAction(verb, directObject, needsHelp); + } + } + } + + /** + * Finds an {@link Arg} given an action name and a long flag name. + * @return The {@link Arg} found or null. + */ + protected Arg findLongArg(String verb, String directObject, String longName) { + if (verb == null) { + verb = GLOBAL_FLAG_VERB; + } + if (directObject == null) { + directObject = NO_VERB_OBJECT; + } + String key = verb + "/" + directObject + "/" + longName; + return mArguments.get(key); + } + + /** + * Finds an {@link Arg} given an action name and a short flag name. + * @return The {@link Arg} found or null. + */ + protected Arg findShortArg(String verb, String directObject, String shortName) { + if (verb == null) { + verb = GLOBAL_FLAG_VERB; + } + if (directObject == null) { + directObject = NO_VERB_OBJECT; + } + + for (Entry<String, Arg> entry : mArguments.entrySet()) { + Arg arg = entry.getValue(); + if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { + if (shortName.equals(arg.getShortArg())) { + return arg; + } + } + } + + return null; + } + + /** + * Prints the help/usage and exits. + * + * @param errorFormat Optional error message to print prior to usage using String.format + * @param args Arguments for String.format + */ + public void printHelpAndExit(String errorFormat, Object... args) { + printHelpAndExitForAction(null /*verb*/, null /*directObject*/, errorFormat, args); + } + + /** + * Prints the help/usage and exits. + * + * @param verb If null, displays help for all verbs. If not null, display help only + * for that specific verb. In all cases also displays general usage and action list. + * @param directObject If null, displays help for all verb objects. + * If not null, displays help only for that specific action + * In all cases also display general usage and action list. + * @param errorFormat Optional error message to print prior to usage using String.format + * @param args Arguments for String.format + */ + public void printHelpAndExitForAction(String verb, String directObject, + String errorFormat, Object... args) { + if (errorFormat != null) { + stderr(errorFormat, args); + } + + /* + * usage should fit in 80 columns + * 12345678901234567890123456789012345678901234567890123456789012345678901234567890 + */ + stdout("\n" + + "Usage:\n" + + " android [global options] action [action options]\n" + + "\n" + + "Global options:"); + listOptions(GLOBAL_FLAG_VERB, NO_VERB_OBJECT); + + if (verb == null || directObject == null) { + stdout("\nValid actions are composed of a verb and an optional direct object:"); + for (String[] action : mActions) { + + stdout("- %1$6s %2$-7s: %3$s", + action[ACTION_VERB_INDEX], + action[ACTION_OBJECT_INDEX], + action[ACTION_DESC_INDEX]); + } + } + + for (String[] action : mActions) { + if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) { + if (directObject == null || directObject.equals(action[ACTION_OBJECT_INDEX])) { + stdout("\nAction \"%1$s %2$s\":", + action[ACTION_VERB_INDEX], + action[ACTION_OBJECT_INDEX]); + stdout(" %1$s", action[ACTION_DESC_INDEX]); + stdout("Options:"); + listOptions(action[ACTION_VERB_INDEX], action[ACTION_OBJECT_INDEX]); + } + } + } + + exit(); + } + + /** + * Internal helper to print all the option flags for a given action name. + */ + protected void listOptions(String verb, String directObject) { + int numOptions = 0; + for (Entry<String, Arg> entry : mArguments.entrySet()) { + Arg arg = entry.getValue(); + if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { + + String value = ""; + if (arg.getDefaultValue() instanceof String[]) { + for (String v : (String[]) arg.getDefaultValue()) { + if (value.length() > 0) { + value += ", "; + } + value += v; + } + } else if (arg.getDefaultValue() != null) { + value = arg.getDefaultValue().toString(); + } + if (value.length() > 0) { + value = " (" + value + ")"; + } + + String required = arg.isMandatory() ? " [required]" : ""; + + stdout(" -%1$s %2$-10s %3$s%4$s%5$s", + arg.getShortArg(), + "--" + arg.getLongArg(), + arg.getDescription(), + value, + required); + numOptions++; + } + } + + if (numOptions == 0) { + stdout(" No options"); + } + } + + //---- + + /** + * The mode of an argument specifies the type of variable it represents, + * whether an extra parameter is required after the flag and how to parse it. + */ + static enum MODE { + /** Argument value is a Boolean. Default value is a Boolean. */ + BOOLEAN { + @Override + public boolean needsExtra() { + return false; + } + @Override + public String process(Arg arg, String extra) { + // Toggle the current value + arg.setCurrentValue(! ((Boolean) arg.getCurrentValue()).booleanValue()); + return null; + } + }, + + /** Argument value is an Integer. Default value is an Integer. */ + INTEGER { + @Override + public boolean needsExtra() { + return true; + } + @Override + public String process(Arg arg, String extra) { + try { + arg.setCurrentValue(Integer.parseInt(extra)); + return null; + } catch (NumberFormatException e) { + return String.format("Failed to parse '%1$s' as an integer: %2%s", + extra, e.getMessage()); + } + } + }, + + /** Argument value is a String. Default value is a String[]. */ + ENUM { + @Override + public boolean needsExtra() { + return true; + } + @Override + public String process(Arg arg, String extra) { + StringBuilder desc = new StringBuilder(); + String[] values = (String[]) arg.getDefaultValue(); + for (String value : values) { + if (value.equals(extra)) { + arg.setCurrentValue(extra); + return null; + } + + if (desc.length() != 0) { + desc.append(", "); + } + desc.append(value); + } + + return String.format("'%1$s' is not one of %2$s", extra, desc.toString()); + } + }, + + /** Argument value is a String. Default value is a null. */ + STRING { + @Override + public boolean needsExtra() { + return true; + } + @Override + public String process(Arg arg, String extra) { + arg.setCurrentValue(extra); + return null; + } + }; + + /** + * Returns true if this mode requires an extra parameter. + */ + public abstract boolean needsExtra(); + + /** + * Processes the flag for this argument. + * + * @param arg The argument being processed. + * @param extra The extra parameter. Null if {@link #needsExtra()} returned false. + * @return An error string or null if there's no error. + */ + public abstract String process(Arg arg, String extra); + } + + /** + * An argument accepted by the command-line, also called "a flag". + * Arguments must have a short version (one letter), a long version name and a description. + * They can have a default value, or it can be null. + * Depending on the {@link MODE}, the default value can be a Boolean, an Integer, a String + * or a String array (in which case the first item is the current by default.) + */ + static class Arg { + /** Verb for that argument. Never null. */ + private final String mVerb; + /** Direct Object for that argument. Never null, but can be empty string. */ + private final String mDirectObject; + /** The 1-letter short name of the argument, e.g. -v. */ + private final String mShortName; + /** The long name of the argument, e.g. --verbose. */ + private final String mLongName; + /** A description. Never null. */ + private final String mDescription; + /** A default value. Can be null. */ + private final Object mDefaultValue; + /** The argument mode (type + process method). Never null. */ + private final MODE mMode; + /** True if this argument is mandatory for this verb/directobject. */ + private final boolean mMandatory; + /** Current value. Initially set to the default value. */ + private Object mCurrentValue; + /** True if the argument has been used on the command line. */ + private boolean mInCommandLine; + + /** + * Creates a new argument flag description. + * + * @param mode The {@link MODE} for the argument. + * @param mandatory True if this argument is mandatory for this action. + * @param directObject The action name. Can be #NO_VERB_OBJECT or #INTERNAL_FLAG. + * @param shortName The one-letter short argument name. Cannot be empty nor null. + * @param longName The long argument name. Cannot be empty nor null. + * @param description The description. Cannot be null. + * @param defaultValue The default value (or values), which depends on the selected {@link MODE}. + */ + public Arg(MODE mode, + boolean mandatory, + String verb, + String directObject, + String shortName, + String longName, + String description, + Object defaultValue) { + mMode = mode; + mMandatory = mandatory; + mVerb = verb; + mDirectObject = directObject; + mShortName = shortName; + mLongName = longName; + mDescription = description; + mDefaultValue = defaultValue; + mInCommandLine = false; + if (defaultValue instanceof String[]) { + mCurrentValue = ((String[])defaultValue)[0]; + } else { + mCurrentValue = mDefaultValue; + } + } + + /** Return true if this argument is mandatory for this verb/directobject. */ + public boolean isMandatory() { + return mMandatory; + } + + /** Returns the 1-letter short name of the argument, e.g. -v. */ + public String getShortArg() { + return mShortName; + } + + /** Returns the long name of the argument, e.g. --verbose. */ + public String getLongArg() { + return mLongName; + } + + /** Returns the description. Never null. */ + public String getDescription() { + return mDescription; + } + + /** Returns the verb for that argument. Never null. */ + public String getVerb() { + return mVerb; + } + + /** Returns the direct Object for that argument. Never null, but can be empty string. */ + public String getDirectObject() { + return mDirectObject; + } + + /** Returns the default value. Can be null. */ + public Object getDefaultValue() { + return mDefaultValue; + } + + /** Returns the current value. Initially set to the default value. Can be null. */ + public Object getCurrentValue() { + return mCurrentValue; + } + + /** Sets the current value. Can be null. */ + public void setCurrentValue(Object currentValue) { + mCurrentValue = currentValue; + } + + /** Returns the argument mode (type + process method). Never null. */ + public MODE getMode() { + return mMode; + } + + /** Returns true if the argument has been used on the command line. */ + public boolean isInCommandLine() { + return mInCommandLine; + } + + /** Sets if the argument has been used on the command line. */ + public void setInCommandLine(boolean inCommandLine) { + mInCommandLine = inCommandLine; + } + } + + /** + * Internal helper to define a new argument for a give action. + * + * @param mode The {@link MODE} for the argument. + * @param verb The verb name. Can be #INTERNAL_VERB. + * @param directObject The action name. Can be #NO_VERB_OBJECT or #INTERNAL_FLAG. + * @param shortName The one-letter short argument name. Cannot be empty nor null. + * @param longName The long argument name. Cannot be empty nor null. + * @param description The description. Cannot be null. + * @param defaultValue The default value (or values), which depends on the selected {@link MODE}. + */ + protected void define(MODE mode, + boolean mandatory, + String verb, + String directObject, + String shortName, String longName, + String description, Object defaultValue) { + assert(mandatory || mode == MODE.BOOLEAN); // a boolean mode cannot be mandatory + + if (directObject == null) { + directObject = NO_VERB_OBJECT; + } + + String key = verb + "/" + directObject + "/" + longName; + mArguments.put(key, new Arg(mode, mandatory, + verb, directObject, shortName, longName, description, defaultValue)); + } + + /** + * Exits in case of error. + * This is protected so that it can be overridden in unit tests. + */ + protected void exit() { + System.exit(1); + } + + /** + * Prints a line to stdout. + * This is protected so that it can be overridden in unit tests. + * + * @param format The string to be formatted. Cannot be null. + * @param args Format arguments. + */ + protected void stdout(String format, Object...args) { + mLog.printf(format + "\n", args); + } + + /** + * Prints a line to stderr. + * This is protected so that it can be overridden in unit tests. + * + * @param format The string to be formatted. Cannot be null. + * @param args Format arguments. + */ + protected void stderr(String format, Object...args) { + mLog.error(null, format, args); + } +} diff --git a/sdkmanager/app/src/com/android/sdkmanager/Main.java b/sdkmanager/app/src/com/android/sdkmanager/Main.java new file mode 100644 index 0000000..154788e --- /dev/null +++ b/sdkmanager/app/src/com/android/sdkmanager/Main.java @@ -0,0 +1,813 @@ +/* + * Copyright (C) 2008 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.sdkmanager; + +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISdkLog; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.SdkManager; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.avd.HardwareProperties; +import com.android.sdklib.avd.AvdManager.AvdInfo; +import com.android.sdklib.avd.HardwareProperties.HardwareProperty; +import com.android.sdklib.project.ProjectCreator; +import com.android.sdklib.project.ProjectCreator.OutputLevel; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Main class for the 'android' application. + */ +class Main { + + /** Java property that defines the location of the sdk/tools directory. */ + private final static String TOOLSDIR = "com.android.sdkmanager.toolsdir"; + /** Java property that defines the working directory. On Windows the current working directory + * is actually the tools dir, in which case this is used to get the original CWD. */ + private final static String WORKDIR = "com.android.sdkmanager.workdir"; + + private final static String[] BOOLEAN_YES_REPLIES = new String[] { "yes", "y" }; + private final static String[] BOOLEAN_NO_REPLIES = new String[] { "no", "n" }; + + /** Path to the SDK folder. This is the parent of {@link #TOOLSDIR}. */ + private String mSdkFolder; + /** Logger object. Use this to print normal output, warnings or errors. */ + private ISdkLog mSdkLog; + /** The SDK manager parses the SDK folder and gives access to the content. */ + private SdkManager mSdkManager; + /** Command-line processor with options specific to SdkManager. */ + private SdkCommandLine mSdkCommandLine; + /** The working directory, either null or set to an existing absolute canonical directory. */ + private File mWorkDir; + + public static void main(String[] args) { + new Main().run(args); + } + + /** + * Runs the sdk manager app + */ + private void run(String[] args) { + createLogger(); + init(); + mSdkCommandLine.parseArgs(args); + parseSdk(); + doAction(); + } + + /** + * Creates the {@link #mSdkLog} object. + * <p/> + * This must be done before {@link #init()} as it will be used to report errors. + */ + private void createLogger() { + mSdkLog = new ISdkLog() { + public void error(Throwable t, String errorFormat, Object... args) { + if (errorFormat != null) { + System.err.printf("Error: " + errorFormat, args); + if (!errorFormat.endsWith("\n")) { + System.err.printf("\n"); + } + } + if (t != null) { + System.err.printf("Error: %s\n", t.getMessage()); + } + } + + public void warning(String warningFormat, Object... args) { + if (mSdkCommandLine.isVerbose()) { + System.out.printf("Warning: " + warningFormat, args); + if (!warningFormat.endsWith("\n")) { + System.out.printf("\n"); + } + } + } + + public void printf(String msgFormat, Object... args) { + System.out.printf(msgFormat, args); + } + }; + } + + /** + * Init the application by making sure the SDK path is available and + * doing basic parsing of the SDK. + */ + private void init() { + mSdkCommandLine = new SdkCommandLine(mSdkLog); + + // We get passed a property for the tools dir + String toolsDirProp = System.getProperty(TOOLSDIR); + if (toolsDirProp == null) { + // for debugging, it's easier to override using the process environment + toolsDirProp = System.getenv(TOOLSDIR); + } + + if (toolsDirProp != null) { + // got back a level for the SDK folder + File tools; + if (toolsDirProp.length() > 0) { + tools = new File(toolsDirProp); + mSdkFolder = tools.getParent(); + } else { + try { + tools = new File(".").getCanonicalFile(); + mSdkFolder = tools.getParent(); + } catch (IOException e) { + // Will print an error below since mSdkFolder is not defined + } + } + } + + if (mSdkFolder == null) { + errorAndExit("The tools directory property is not set, please make sure you are executing %1$s", + SdkConstants.androidCmdName()); + } + + // We might get passed a property for the working directory + // Either it is a valid directory and mWorkDir is set to it's absolute canonical value + // or mWorkDir remains null. + String workDirProp = System.getProperty(WORKDIR); + if (workDirProp == null) { + workDirProp = System.getenv(WORKDIR); + } + if (workDirProp != null) { + // This should be a valid directory + mWorkDir = new File(workDirProp); + try { + mWorkDir = mWorkDir.getCanonicalFile().getAbsoluteFile(); + } catch (IOException e) { + mWorkDir = null; + } + if (mWorkDir == null || !mWorkDir.isDirectory()) { + errorAndExit("The working directory does not seem to be valid: '%1$s", workDirProp); + } + } + } + + /** + * Does the basic SDK parsing required for all actions + */ + private void parseSdk() { + mSdkManager = SdkManager.createManager(mSdkFolder, mSdkLog); + + if (mSdkManager == null) { + errorAndExit("Unable to parse SDK content."); + } + } + + /** + * Actually do an action... + */ + private void doAction() { + String verb = mSdkCommandLine.getVerb(); + String directObject = mSdkCommandLine.getDirectObject(); + + if (SdkCommandLine.VERB_LIST.equals(verb)) { + // list action. + if (SdkCommandLine.OBJECT_TARGET.equals(directObject)) { + displayTargetList(); + } else if (SdkCommandLine.OBJECT_AVD.equals(directObject)) { + displayAvdList(); + } else { + displayTargetList(); + displayAvdList(); + } + + } else if (SdkCommandLine.VERB_CREATE.equals(verb) && + SdkCommandLine.OBJECT_AVD.equals(directObject)) { + createAvd(); + + } else if (SdkCommandLine.VERB_DELETE.equals(verb) && + SdkCommandLine.OBJECT_AVD.equals(directObject)) { + deleteAvd(); + + } else if (SdkCommandLine.VERB_MOVE.equals(verb) && + SdkCommandLine.OBJECT_AVD.equals(directObject)) { + moveAvd(); + + } else if (SdkCommandLine.VERB_CREATE.equals(verb) && + SdkCommandLine.OBJECT_PROJECT.equals(directObject)) { + createProject(); + + } else if (SdkCommandLine.VERB_UPDATE.equals(verb) && + SdkCommandLine.OBJECT_PROJECT.equals(directObject)) { + updateProject(); + } else { + mSdkCommandLine.printHelpAndExit(null); + } + } + + /** + * Creates a new Android project based on command-line parameters + */ + private void createProject() { + // get the target and try to resolve it. + int targetId = mSdkCommandLine.getParamTargetId(); + IAndroidTarget[] targets = mSdkManager.getTargets(); + if (targetId < 1 || targetId > targets.length) { + errorAndExit("Target id is not valid. Use '%s list targets' to get the target ids.", + SdkConstants.androidCmdName()); + } + IAndroidTarget target = targets[targetId - 1]; + + ProjectCreator creator = new ProjectCreator(mSdkFolder, + mSdkCommandLine.isVerbose() ? OutputLevel.VERBOSE : + mSdkCommandLine.isSilent() ? OutputLevel.SILENT : + OutputLevel.NORMAL, + mSdkLog); + + String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); + + creator.createProject(projectDir, + mSdkCommandLine.getParamName(), + mSdkCommandLine.getParamProjectPackage(), + mSdkCommandLine.getParamProjectActivity(), + target, + false /* isTestProject*/); + } + + /** + * Updates an existing Android project based on command-line parameters + */ + private void updateProject() { + // get the target and try to resolve it. + IAndroidTarget target = null; + int targetId = mSdkCommandLine.getParamTargetId(); + if (targetId >= 0) { + IAndroidTarget[] targets = mSdkManager.getTargets(); + if (targetId < 1 || targetId > targets.length) { + errorAndExit("Target id is not valid. Use '%s list targets' to get the target ids.", + SdkConstants.androidCmdName()); + } + target = targets[targetId - 1]; + } + + ProjectCreator creator = new ProjectCreator(mSdkFolder, + mSdkCommandLine.isVerbose() ? OutputLevel.VERBOSE : + mSdkCommandLine.isSilent() ? OutputLevel.SILENT : + OutputLevel.NORMAL, + mSdkLog); + + String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); + + creator.updateProject(projectDir, + target, + mSdkCommandLine.getParamName()); + } + + /** + * Adjusts the project location to make it absolute & canonical relative to the + * working directory, if any. + * + * @return The project absolute path relative to {@link #mWorkDir} or the original + * newProjectLocation otherwise. + */ + private String getProjectLocation(String newProjectLocation) { + + // If the new project location is absolute, use it as-is + File projectDir = new File(newProjectLocation); + if (projectDir.isAbsolute()) { + return newProjectLocation; + } + + // if there's no working directory, just use the project location as-is. + if (mWorkDir == null) { + return newProjectLocation; + } + + // Combine then and get an absolute canonical directory + try { + projectDir = new File(mWorkDir, newProjectLocation).getCanonicalFile(); + + return projectDir.getPath(); + } catch (IOException e) { + errorAndExit("Failed to combine working directory '%1$s' with project location '%2$s': %3$s", + mWorkDir.getPath(), + newProjectLocation, + e.getMessage()); + return null; + } + } + + /** + * Displays the list of available Targets (Platforms and Add-ons) + */ + private void displayTargetList() { + mSdkLog.printf("Available Android targets:\n"); + + int index = 1; + for (IAndroidTarget target : mSdkManager.getTargets()) { + if (target.isPlatform()) { + mSdkLog.printf("[%d] %s\n", index, target.getName()); + mSdkLog.printf(" API level: %d\n", target.getApiVersionNumber()); + } else { + mSdkLog.printf("[%d] Add-on: %s\n", index, target.getName()); + mSdkLog.printf(" Vendor: %s\n", target.getVendor()); + if (target.getDescription() != null) { + mSdkLog.printf(" Description: %s\n", target.getDescription()); + } + mSdkLog.printf(" Based on Android %s (API level %d)\n", + target.getApiVersionName(), target.getApiVersionNumber()); + + // display the optional libraries. + IOptionalLibrary[] libraries = target.getOptionalLibraries(); + if (libraries != null) { + mSdkLog.printf(" Libraries:\n"); + for (IOptionalLibrary library : libraries) { + mSdkLog.printf(" * %1$s (%2$s)\n", + library.getName(), library.getJarName()); + mSdkLog.printf(String.format( + " %1$s\n", library.getDescription())); + } + } + } + + // get the target skins + displaySkinList(target, " Skins: "); + + index++; + } + } + + /** + * Displays the skins valid for the given target. + */ + private void displaySkinList(IAndroidTarget target, String message) { + String[] skins = target.getSkins(); + String defaultSkin = target.getDefaultSkin(); + mSdkLog.printf(message); + if (skins != null) { + boolean first = true; + for (String skin : skins) { + if (first == false) { + mSdkLog.printf(", "); + } else { + first = false; + } + mSdkLog.printf(skin); + + if (skin.equals(defaultSkin)) { + mSdkLog.printf(" (default)"); + } + } + mSdkLog.printf("\n"); + } else { + mSdkLog.printf("no skins.\n"); + } + } + + /** + * Displays the list of available AVDs. + */ + private void displayAvdList() { + try { + AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); + + mSdkLog.printf("Available Android Virtual Devices:\n"); + + AvdInfo[] avds = avdManager.getAvds(); + for (int index = 0 ; index < avds.length ; index++) { + AvdInfo info = avds[index]; + if (index > 0) { + mSdkLog.printf("---------\n"); + } + mSdkLog.printf(" Name: %s\n", info.getName()); + mSdkLog.printf(" Path: %s\n", info.getPath()); + + // get the target of the AVD + IAndroidTarget target = info.getTarget(); + if (target.isPlatform()) { + mSdkLog.printf(" Target: %s (API level %d)\n", target.getName(), + target.getApiVersionNumber()); + } else { + mSdkLog.printf(" Target: %s (%s)\n", target.getName(), target + .getVendor()); + mSdkLog.printf(" Based on Android %s (API level %d)\n", target + .getApiVersionName(), target.getApiVersionNumber()); + } + + // display some extra values. + Map<String, String> properties = info.getProperties(); + String skin = properties.get(AvdManager.AVD_INI_SKIN_NAME); + if (skin != null) { + mSdkLog.printf(" Skin: %s\n", skin); + } + String sdcard = properties.get(AvdManager.AVD_INI_SDCARD_SIZE); + if (sdcard == null) { + sdcard = properties.get(AvdManager.AVD_INI_SDCARD_PATH); + } + if (sdcard != null) { + mSdkLog.printf(" Sdcard: %s\n", sdcard); + } + } + } catch (AndroidLocationException e) { + errorAndExit(e.getMessage()); + } + } + + /** + * Creates a new AVD. This is a text based creation with command line prompt. + */ + private void createAvd() { + // find a matching target + int targetId = mSdkCommandLine.getParamTargetId(); + IAndroidTarget target = null; + + if (targetId >= 1 && targetId <= mSdkManager.getTargets().length) { + target = mSdkManager.getTargets()[targetId-1]; // target it is 1-based + } else { + errorAndExit("Target id is not valid. Use '%s list targets' to get the target ids.", + SdkConstants.androidCmdName()); + } + + try { + boolean removePrevious = false; + AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); + + String avdName = mSdkCommandLine.getParamName(); + AvdInfo info = avdManager.getAvd(avdName); + if (info != null) { + if (mSdkCommandLine.getFlagForce()) { + removePrevious = true; + mSdkLog.warning( + "Android Virtual Device '%s' already exists and will be replaced.", + avdName); + } else { + errorAndExit("Android Virtual Device '%s' already exists.", avdName); + return; + } + } + + String paramFolderPath = mSdkCommandLine.getParamLocationPath(); + File avdFolder = null; + if (paramFolderPath != null) { + avdFolder = new File(paramFolderPath); + } else { + avdFolder = new File(AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, + avdName + AvdManager.AVD_FOLDER_EXTENSION); + } + + Map<String, String> hardwareConfig = null; + if (target.isPlatform()) { + try { + hardwareConfig = promptForHardware(target); + } catch (IOException e) { + errorAndExit(e.getMessage()); + } + } + + AvdInfo oldAvdInfo = null; + if (removePrevious) { + oldAvdInfo = avdManager.getAvd(avdName); + } + + // Validate skin is either default (empty) or NNNxMMM or a valid skin name. + String skin = mSdkCommandLine.getParamSkin(); + if (skin != null && skin.length() == 0) { + skin = null; + } + if (skin != null) { + boolean valid = false; + // Is it a know skin name for this target? + for (String s : target.getSkins()) { + if (skin.equalsIgnoreCase(s)) { + skin = s; // Make skin names case-insensitive. + valid = true; + break; + } + } + + // Is it NNNxMMM? + if (!valid) { + valid = AvdManager.NUMERIC_SKIN_SIZE.matcher(skin).matches(); + } + + if (!valid) { + displaySkinList(target, "Valid skins: "); + errorAndExit("'%s' is not a valid skin name or size (NNNxMMM)", skin); + return; + } + } + + AvdInfo newAvdInfo = avdManager.createAvd(avdFolder, + avdName, + target, + skin, + mSdkCommandLine.getParamSdCard(), + hardwareConfig, + removePrevious, + mSdkLog); + + if (newAvdInfo != null && + oldAvdInfo != null && + !oldAvdInfo.getPath().equals(newAvdInfo.getPath())) { + mSdkLog.warning("Removing previous AVD directory at %s", oldAvdInfo.getPath()); + // Remove the old data directory + File dir = new File(oldAvdInfo.getPath()); + avdManager.recursiveDelete(dir); + dir.delete(); + // Remove old avd info from manager + avdManager.removeAvd(oldAvdInfo); + } + + } catch (AndroidLocationException e) { + errorAndExit(e.getMessage()); + } + } + + /** + * Delete an AVD. + */ + private void deleteAvd() { + try { + String avdName = mSdkCommandLine.getParamName(); + AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); + AvdInfo info = avdManager.getAvd(avdName); + + if (info == null) { + errorAndExit("There is no Android Virtual Device named '%s'.", avdName); + return; + } + + avdManager.deleteAvd(info, mSdkLog); + } catch (AndroidLocationException e) { + errorAndExit(e.getMessage()); + } + } + + /** + * Move an AVD. + */ + private void moveAvd() { + try { + String avdName = mSdkCommandLine.getParamName(); + AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); + AvdInfo info = avdManager.getAvd(avdName); + + if (info == null) { + errorAndExit("There is no Android Virtual Device named '%s'.", avdName); + return; + } + + // This is a rename if there's a new name for the AVD + String newName = mSdkCommandLine.getParamMoveNewName(); + if (newName != null && newName.equals(info.getName())) { + // same name, not actually a rename operation + newName = null; + } + + // This is a move (of the data files) if there's a new location path + String paramFolderPath = mSdkCommandLine.getParamLocationPath(); + if (paramFolderPath != null) { + // check if paths are the same. Use File methods to account for OS idiosyncrasies. + try { + File f1 = new File(paramFolderPath).getCanonicalFile(); + File f2 = new File(info.getPath()).getCanonicalFile(); + if (f1.equals(f2)) { + // same canonical path, so not actually a move + paramFolderPath = null; + } + } catch (IOException e) { + // Fail to resolve canonical path. Fail now since a move operation might fail + // later and be harder to recover from. + errorAndExit(e.getMessage()); + return; + } + } + + if (newName == null && paramFolderPath == null) { + mSdkLog.warning("Move operation aborted: same AVD name, same canonical data path"); + return; + } + + // If a rename was requested and no data move was requested, check if the original + // data path is our default constructed from the AVD name. In this case we still want + // to rename that folder too. + if (newName != null && paramFolderPath == null) { + // Compute the original data path + File originalFolder = new File( + AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, + info.getName() + AvdManager.AVD_FOLDER_EXTENSION); + if (originalFolder.equals(info.getPath())) { + try { + // The AVD is using the default data folder path based on the AVD name. + // That folder needs to be adjusted to use the new name. + File f = new File(AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, + newName + AvdManager.AVD_FOLDER_EXTENSION); + paramFolderPath = f.getCanonicalPath(); + } catch (IOException e) { + // Fail to resolve canonical path. Fail now rather than later. + errorAndExit(e.getMessage()); + } + } + } + + // Check for conflicts + + if (newName != null && avdManager.getAvd(newName) != null) { + errorAndExit("There is already an AVD named '%s'.", newName); + return; + } + if (newName != null) { + if (avdManager.getAvd(newName) != null) { + errorAndExit("There is already an AVD named '%s'.", newName); + return; + } + + File ini = info.getIniFile(); + if (ini.equals(AvdInfo.getIniFile(newName))) { + errorAndExit("The AVD file '%s' is in the way.", ini.getCanonicalPath()); + return; + } + } + + if (paramFolderPath != null && new File(paramFolderPath).exists()) { + errorAndExit( + "There is already a file or directory at '%s'.\nUse --path to specify a different data folder.", + paramFolderPath); + } + + avdManager.moveAvd(info, newName, paramFolderPath, mSdkLog); + } catch (AndroidLocationException e) { + errorAndExit(e.getMessage()); + } catch (IOException e) { + errorAndExit(e.getMessage()); + } + } + + /** + * Prompts the user to setup a hardware config for a Platform-based AVD. + * @throws IOException + */ + private Map<String, String> promptForHardware(IAndroidTarget createTarget) throws IOException { + byte[] readLineBuffer = new byte[256]; + String result; + String defaultAnswer = "no"; + + mSdkLog.printf("%s is a basic Android platform.\n", createTarget.getName()); + mSdkLog.printf("Do you wish to create a custom hardware profile [%s]", + defaultAnswer); + + result = readLine(readLineBuffer).trim(); + // handle default: + if (result.length() == 0) { + result = defaultAnswer; + } + + if (getBooleanReply(result) == false) { + // no custom config. + return null; + } + + mSdkLog.printf("\n"); // empty line + + // get the list of possible hardware properties + File hardwareDefs = new File (mSdkFolder + File.separator + + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER, SdkConstants.FN_HARDWARE_INI); + List<HardwareProperty> list = HardwareProperties.parseHardwareDefinitions(hardwareDefs, + null /*sdkLog*/); + + HashMap<String, String> map = new HashMap<String, String>(); + + for (int i = 0 ; i < list.size() ;) { + HardwareProperty property = list.get(i); + + String description = property.getDescription(); + if (description != null) { + mSdkLog.printf("%s: %s\n", property.getAbstract(), description); + } else { + mSdkLog.printf("%s\n", property.getAbstract()); + } + + String defaultValue = property.getDefault(); + + if (defaultValue != null) { + mSdkLog.printf("%s [%s]:", property.getName(), defaultValue); + } else { + mSdkLog.printf("%s (%s):", property.getName(), property.getType()); + } + + result = readLine(readLineBuffer); + if (result.length() == 0) { + if (defaultValue != null) { + mSdkLog.printf("\n"); // empty line + i++; // go to the next property if we have a valid default value. + // if there's no default, we'll redo this property + } + continue; + } + + switch (property.getType()) { + case BOOLEAN: + try { + if (getBooleanReply(result)) { + map.put(property.getName(), "yes"); + i++; // valid reply, move to next property + } else { + map.put(property.getName(), "no"); + i++; // valid reply, move to next property + } + } catch (IOException e) { + // display error, and do not increment i to redo this property + mSdkLog.printf("\n%s\n", e.getMessage()); + } + break; + case INTEGER: + try { + Integer.parseInt(result); + map.put(property.getName(), result); + i++; // valid reply, move to next property + } catch (NumberFormatException e) { + // display error, and do not increment i to redo this property + mSdkLog.printf("\n%s\n", e.getMessage()); + } + break; + case DISKSIZE: + // TODO check validity + map.put(property.getName(), result); + i++; // valid reply, move to next property + break; + } + + mSdkLog.printf("\n"); // empty line + } + + return map; + } + + /** + * Reads the line from the input stream. + * @param buffer + * @throws IOException + */ + private String readLine(byte[] buffer) throws IOException { + int count = System.in.read(buffer); + + // is the input longer than the buffer? + if (count == buffer.length && buffer[count-1] != 10) { + // create a new temp buffer + byte[] tempBuffer = new byte[256]; + + // and read the rest + String secondHalf = readLine(tempBuffer); + + // return a concat of both + return new String(buffer, 0, count) + secondHalf; + } + + // ignore end whitespace + while (count > 0 && (buffer[count-1] == '\r' || buffer[count-1] == '\n')) { + count--; + } + + return new String(buffer, 0, count); + } + + /** + * Returns the boolean value represented by the string. + * @throws IOException If the value is not a boolean string. + */ + private boolean getBooleanReply(String reply) throws IOException { + + for (String valid : BOOLEAN_YES_REPLIES) { + if (valid.equalsIgnoreCase(reply)) { + return true; + } + } + + for (String valid : BOOLEAN_NO_REPLIES) { + if (valid.equalsIgnoreCase(reply)) { + return false; + } + } + + throw new IOException(String.format("%s is not a valid reply", reply)); + } + + private void errorAndExit(String format, Object...args) { + mSdkLog.error(null, format, args); + System.exit(1); + } +}
\ No newline at end of file diff --git a/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java b/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java new file mode 100644 index 0000000..34a69bd --- /dev/null +++ b/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2008 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.sdkmanager; + +import com.android.sdklib.ISdkLog; +import com.android.sdklib.SdkManager; + + +/** + * Specific command-line flags for the {@link SdkManager}. + */ +public class SdkCommandLine extends CommandLineProcessor { + + public final static String VERB_LIST = "list"; + public final static String VERB_CREATE = "create"; + public final static String VERB_MOVE = "move"; + public final static String VERB_DELETE = "delete"; + public final static String VERB_UPDATE = "update"; + + public static final String OBJECT_AVD = "avd"; + public static final String OBJECT_AVDS = "avds"; + public static final String OBJECT_TARGET = "target"; + public static final String OBJECT_TARGETS = "targets"; + public static final String OBJECT_PROJECT = "project"; + + public static final String ARG_ALIAS = "alias"; + public static final String ARG_ACTIVITY = "activity"; + + public static final String KEY_ACTIVITY = ARG_ACTIVITY; + public static final String KEY_PACKAGE = "package"; + public static final String KEY_MODE = "mode"; + public static final String KEY_TARGET_ID = OBJECT_TARGET; + public static final String KEY_NAME = "name"; + public static final String KEY_PATH = "path"; + public static final String KEY_FILTER = "filter"; + public static final String KEY_SKIN = "skin"; + public static final String KEY_SDCARD = "sdcard"; + public static final String KEY_FORCE = "force"; + public static final String KEY_RENAME = "rename"; + + /** + * Action definitions for SdkManager command line. + * <p/> + * Each entry is a string array with: + * <ul> + * <li> the verb. + * <li> an object (use #NO_VERB_OBJECT if there's no object). + * <li> a description. + * <li> an alternate form for the object (e.g. plural). + * </ul> + */ + private final static String[][] ACTIONS = { + { VERB_LIST, NO_VERB_OBJECT, + "Lists existing targets or virtual devices." }, + { VERB_LIST, OBJECT_AVD, + "Lists existing Android Virtual Devices.", + OBJECT_AVDS }, + { VERB_LIST, OBJECT_TARGET, + "Lists existing targets.", + OBJECT_TARGETS }, + + { VERB_CREATE, OBJECT_AVD, + "Creates a new Android Virtual Device." }, + { VERB_MOVE, OBJECT_AVD, + "Moves or renames an Android Virtual Device." }, + { VERB_DELETE, OBJECT_AVD, + "Deletes an Android Virtual Device." }, + + { VERB_CREATE, OBJECT_PROJECT, + "Creates a new Android Project." }, + { VERB_UPDATE, OBJECT_PROJECT, + "Updates an Android Project (must have an AndroidManifest.xml)." }, + }; + + public SdkCommandLine(ISdkLog logger) { + super(logger, ACTIONS); + + // --- create avd --- + + define(MODE.STRING, false, + VERB_CREATE, OBJECT_AVD, "p", KEY_PATH, + "Location path of the directory where the new AVD will be created", null); + define(MODE.STRING, true, + VERB_CREATE, OBJECT_AVD, "n", KEY_NAME, + "Name of the new AVD", null); + define(MODE.INTEGER, true, + VERB_CREATE, OBJECT_AVD, "t", KEY_TARGET_ID, + "Target id of the new AVD", null); + define(MODE.STRING, false, + VERB_CREATE, OBJECT_AVD, "s", KEY_SKIN, + "Skin of the new AVD", null); + define(MODE.STRING, false, + VERB_CREATE, OBJECT_AVD, "c", KEY_SDCARD, + "Path to a shared SD card image, or size of a new sdcard for the new AVD", null); + define(MODE.BOOLEAN, false, + VERB_CREATE, OBJECT_AVD, "f", KEY_FORCE, + "Force creation (override an existing AVD)", false); + + // --- delete avd --- + + define(MODE.STRING, true, + VERB_DELETE, OBJECT_AVD, "n", KEY_NAME, + "Name of the AVD to delete", null); + + // --- move avd --- + + define(MODE.STRING, true, + VERB_MOVE, OBJECT_AVD, "n", KEY_NAME, + "Name of the AVD to move or rename", null); + define(MODE.STRING, false, + VERB_MOVE, OBJECT_AVD, "r", KEY_RENAME, + "New name of the AVD to rename", null); + define(MODE.STRING, false, + VERB_MOVE, OBJECT_AVD, "p", KEY_PATH, + "New location path of the directory where to move the AVD", null); + + // --- create project --- + + define(MODE.ENUM, true, + VERB_CREATE, OBJECT_PROJECT, "m", KEY_MODE, + "Project mode", new String[] { ARG_ACTIVITY, ARG_ALIAS }); + define(MODE.STRING, true, + VERB_CREATE, OBJECT_PROJECT, + "p", KEY_PATH, + "Location path of new project", null); + define(MODE.INTEGER, true, + VERB_CREATE, OBJECT_PROJECT, "t", KEY_TARGET_ID, + "Target id of the new project", null); + define(MODE.STRING, true, + VERB_CREATE, OBJECT_PROJECT, "k", KEY_PACKAGE, + "Package name", null); + define(MODE.STRING, true, + VERB_CREATE, OBJECT_PROJECT, "a", KEY_ACTIVITY, + "Activity name", null); + define(MODE.STRING, false, + VERB_CREATE, OBJECT_PROJECT, "n", KEY_NAME, + "Project name", null); + + // --- update project --- + + define(MODE.STRING, true, + VERB_UPDATE, OBJECT_PROJECT, + "p", KEY_PATH, + "Location path of the project", null); + define(MODE.INTEGER, true, + VERB_UPDATE, OBJECT_PROJECT, + "t", KEY_TARGET_ID, + "Target id to set for the project", -1); + define(MODE.STRING, false, + VERB_UPDATE, OBJECT_PROJECT, + "n", KEY_NAME, + "Project name", null); + } + + // -- some helpers for generic action flags + + /** Helper to retrieve the --path value. */ + public String getParamLocationPath() { + return ((String) getValue(null, null, KEY_PATH)); + } + + /** Helper to retrieve the --target id value. */ + public int getParamTargetId() { + return ((Integer) getValue(null, null, KEY_TARGET_ID)).intValue(); + } + + /** Helper to retrieve the --name value. */ + public String getParamName() { + return ((String) getValue(null, null, KEY_NAME)); + } + + /** Helper to retrieve the --skin value. */ + public String getParamSkin() { + return ((String) getValue(null, null, KEY_SKIN)); + } + + /** Helper to retrieve the --sdcard value. */ + public String getParamSdCard() { + return ((String) getValue(null, null, KEY_SDCARD)); + } + + /** Helper to retrieve the --force flag. */ + public boolean getFlagForce() { + return ((Boolean) getValue(null, null, KEY_FORCE)).booleanValue(); + } + + // -- some helpers for avd action flags + + /** Helper to retrieve the --rename value for a move verb. */ + public String getParamMoveNewName() { + return ((String) getValue(VERB_MOVE, null, KEY_RENAME)); + } + + + // -- some helpers for project action flags + + /** Helper to retrieve the --package value. */ + public String getParamProjectPackage() { + return ((String) getValue(null, OBJECT_PROJECT, KEY_PACKAGE)); + } + + /** Helper to retrieve the --activity for the new project action. */ + public String getParamProjectActivity() { + return ((String) getValue(null, OBJECT_PROJECT, KEY_ACTIVITY)); + } +} diff --git a/sdkmanager/app/tests/com/android/sdkmanager/CommandLineProcessorTest.java b/sdkmanager/app/tests/com/android/sdkmanager/CommandLineProcessorTest.java new file mode 100644 index 0000000..918591b --- /dev/null +++ b/sdkmanager/app/tests/com/android/sdkmanager/CommandLineProcessorTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2008 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.sdkmanager; + +import com.android.sdklib.ISdkLog; + +import junit.framework.TestCase; + + +public class CommandLineProcessorTest extends TestCase { + + private MockStdLogger mLog; + + /** + * A mock version of the {@link CommandLineProcessor} class that does not + * exits and captures its stdout/stderr output. + */ + public static class MockCommandLineProcessor extends CommandLineProcessor { + private boolean mExitCalled; + private boolean mHelpCalled; + private String mStdOut = ""; + private String mStdErr = ""; + + public MockCommandLineProcessor(ISdkLog logger) { + super(logger, + new String[][] { + { "verb1", "action1", "Some action" }, + { "verb1", "action2", "Another action" }, + }); + define(MODE.STRING, false /*mandatory*/, + "verb1", "action1", "1", "first", "non-mandatory flag", null); + define(MODE.STRING, true /*mandatory*/, + "verb1", "action1", "2", "second", "mandatory flag", null); + } + + @Override + public void printHelpAndExitForAction(String verb, String directObject, + String errorFormat, Object... args) { + mHelpCalled = true; + super.printHelpAndExitForAction(verb, directObject, errorFormat, args); + } + + @Override + protected void exit() { + mExitCalled = true; + } + + @Override + protected void stdout(String format, Object... args) { + String s = String.format(format, args); + mStdOut += s + "\n"; + // don't call super to avoid printing stuff + } + + @Override + protected void stderr(String format, Object... args) { + String s = String.format(format, args); + mStdErr += s + "\n"; + // don't call super to avoid printing stuff + } + + public boolean wasHelpCalled() { + return mHelpCalled; + } + + public boolean wasExitCalled() { + return mExitCalled; + } + + public String getStdOut() { + return mStdOut; + } + + public String getStdErr() { + return mStdErr; + } + } + + @Override + protected void setUp() throws Exception { + mLog = new MockStdLogger(); + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public final void testPrintHelpAndExit() { + MockCommandLineProcessor c = new MockCommandLineProcessor(mLog); + assertFalse(c.wasExitCalled()); + assertFalse(c.wasHelpCalled()); + assertTrue(c.getStdOut().equals("")); + assertTrue(c.getStdErr().equals("")); + c.printHelpAndExit(null); + assertTrue(c.getStdOut().indexOf("-v") != -1); + assertTrue(c.getStdOut().indexOf("--verbose") != -1); + assertTrue(c.getStdErr().equals("")); + assertTrue(c.wasExitCalled()); + + c = new MockCommandLineProcessor(mLog); + assertFalse(c.wasExitCalled()); + assertTrue(c.getStdOut().equals("")); + assertTrue(c.getStdErr().indexOf("Missing parameter") == -1); + + c.printHelpAndExit("Missing %s", "parameter"); + assertTrue(c.wasExitCalled()); + assertFalse(c.getStdOut().equals("")); + assertTrue(c.getStdErr().indexOf("Missing parameter") != -1); + } + + public final void testVerbose() { + MockCommandLineProcessor c = new MockCommandLineProcessor(mLog); + + assertFalse(c.isVerbose()); + c.parseArgs(new String[] { "-v" }); + assertTrue(c.isVerbose()); + assertTrue(c.wasExitCalled()); + assertTrue(c.wasHelpCalled()); + assertTrue(c.getStdErr().indexOf("Missing verb name.") != -1); + + c = new MockCommandLineProcessor(mLog); + c.parseArgs(new String[] { "--verbose" }); + assertTrue(c.isVerbose()); + assertTrue(c.wasExitCalled()); + assertTrue(c.wasHelpCalled()); + assertTrue(c.getStdErr().indexOf("Missing verb name.") != -1); + } + + public final void testHelp() { + MockCommandLineProcessor c = new MockCommandLineProcessor(mLog); + + c.parseArgs(new String[] { "-h" }); + assertTrue(c.wasExitCalled()); + assertTrue(c.wasHelpCalled()); + assertTrue(c.getStdErr().indexOf("Missing verb name.") == -1); + + c = new MockCommandLineProcessor(mLog); + c.parseArgs(new String[] { "--help" }); + assertTrue(c.wasExitCalled()); + assertTrue(c.wasHelpCalled()); + assertTrue(c.getStdErr().indexOf("Missing verb name.") == -1); + } + + public final void testMandatory() { + MockCommandLineProcessor c = new MockCommandLineProcessor(mLog); + + c.parseArgs(new String[] { "verb1", "action1", "-1", "value1", "-2", "value2" }); + assertFalse(c.wasExitCalled()); + assertFalse(c.wasHelpCalled()); + assertEquals("", c.getStdErr()); + assertEquals("value1", c.getValue("verb1", "action1", "first")); + assertEquals("value2", c.getValue("verb1", "action1", "second")); + + c = new MockCommandLineProcessor(mLog); + c.parseArgs(new String[] { "verb1", "action1", "-2", "value2" }); + assertFalse(c.wasExitCalled()); + assertFalse(c.wasHelpCalled()); + assertEquals("", c.getStdErr()); + assertEquals(null, c.getValue("verb1", "action1", "first")); + assertEquals("value2", c.getValue("verb1", "action1", "second")); + + c = new MockCommandLineProcessor(mLog); + c.parseArgs(new String[] { "verb1", "action1" }); + assertTrue(c.wasExitCalled()); + assertTrue(c.wasHelpCalled()); + assertTrue(c.getStdErr().indexOf("must be defined") != -1); + assertEquals(null, c.getValue("verb1", "action1", "first")); + assertEquals(null, c.getValue("verb1", "action1", "second")); + } +} diff --git a/sdkmanager/app/tests/com/android/sdkmanager/MockStdLogger.java b/sdkmanager/app/tests/com/android/sdkmanager/MockStdLogger.java new file mode 100644 index 0000000..961e88d --- /dev/null +++ b/sdkmanager/app/tests/com/android/sdkmanager/MockStdLogger.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.sdkmanager; + +import com.android.sdklib.ISdkLog; + +/** + * + */ +public class MockStdLogger implements ISdkLog { + + public void error(Throwable t, String errorFormat, Object... args) { + if (errorFormat != null) { + System.err.printf("Error: " + errorFormat, args); + if (!errorFormat.endsWith("\n")) { + System.err.printf("\n"); + } + } + if (t != null) { + System.err.printf("Error: %s\n", t.getMessage()); + } + } + + public void warning(String warningFormat, Object... args) { + System.out.printf("Warning: " + warningFormat, args); + if (!warningFormat.endsWith("\n")) { + System.out.printf("\n"); + } + } + + public void printf(String msgFormat, Object... args) { + System.out.printf(msgFormat, args); + } +} diff --git a/sdkmanager/app/tests/com/android/sdkmanager/SdkCommandLineTest.java b/sdkmanager/app/tests/com/android/sdkmanager/SdkCommandLineTest.java new file mode 100644 index 0000000..07a32e0 --- /dev/null +++ b/sdkmanager/app/tests/com/android/sdkmanager/SdkCommandLineTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2008 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.sdkmanager; + +import com.android.sdklib.ISdkLog; + +import junit.framework.TestCase; + +public class SdkCommandLineTest extends TestCase { + + private MockStdLogger mLog; + + /** + * A mock version of the {@link SdkCommandLine} class that does not + * exits and discards its stdout/stderr output. + */ + public static class MockSdkCommandLine extends SdkCommandLine { + private boolean mExitCalled; + private boolean mHelpCalled; + + public MockSdkCommandLine(ISdkLog logger) { + super(logger); + } + + @Override + public void printHelpAndExitForAction(String verb, String directObject, + String errorFormat, Object... args) { + mHelpCalled = true; + super.printHelpAndExitForAction(verb, directObject, errorFormat, args); + } + + @Override + protected void exit() { + mExitCalled = true; + } + + @Override + protected void stdout(String format, Object... args) { + // discard + } + + @Override + protected void stderr(String format, Object... args) { + // discard + } + + public boolean wasExitCalled() { + return mExitCalled; + } + + public boolean wasHelpCalled() { + return mHelpCalled; + } + } + + @Override + protected void setUp() throws Exception { + mLog = new MockStdLogger(); + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + /** Test list */ + public final void testList_Avd_Verbose() { + MockSdkCommandLine c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "-v", "list", "avd" }); + assertFalse(c.wasHelpCalled()); + assertFalse(c.wasExitCalled()); + assertEquals("list", c.getVerb()); + assertEquals("avd", c.getDirectObject()); + assertTrue(c.isVerbose()); + } + + public final void testList_Target() { + MockSdkCommandLine c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "list", "target" }); + assertFalse(c.wasHelpCalled()); + assertFalse(c.wasExitCalled()); + assertEquals("list", c.getVerb()); + assertEquals("target", c.getDirectObject()); + assertFalse(c.isVerbose()); + } + + public final void testList_None() { + MockSdkCommandLine c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "list" }); + assertFalse(c.wasHelpCalled()); + assertFalse(c.wasExitCalled()); + assertEquals("list", c.getVerb()); + assertEquals("", c.getDirectObject()); + assertFalse(c.isVerbose()); + } + + public final void testList_Invalid() { + MockSdkCommandLine c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "list", "unknown" }); + assertTrue(c.wasHelpCalled()); + assertTrue(c.wasExitCalled()); + assertEquals(null, c.getVerb()); + assertEquals(null, c.getDirectObject()); + assertFalse(c.isVerbose()); + } + + public final void testList_Plural() { + MockSdkCommandLine c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "list", "avds" }); + assertFalse(c.wasHelpCalled()); + assertFalse(c.wasExitCalled()); + assertEquals("list", c.getVerb()); + // we get the non-plural form + assertEquals("avd", c.getDirectObject()); + assertFalse(c.isVerbose()); + + c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "list", "targets" }); + assertFalse(c.wasHelpCalled()); + assertFalse(c.wasExitCalled()); + assertEquals("list", c.getVerb()); + // we get the non-plural form + assertEquals("target", c.getDirectObject()); + assertFalse(c.isVerbose()); + } +} diff --git a/sdkmanager/libs/Android.mk b/sdkmanager/libs/Android.mk new file mode 100644 index 0000000..a934aa7 --- /dev/null +++ b/sdkmanager/libs/Android.mk @@ -0,0 +1,18 @@ +# +# Copyright (C) 2008 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. +# +SDKLIBS_LOCAL_DIR := $(call my-dir) +include $(SDKLIBS_LOCAL_DIR)/sdklib/Android.mk +include $(SDKLIBS_LOCAL_DIR)/sdkuilib/Android.mk diff --git a/sdkmanager/libs/sdklib/.classpath b/sdkmanager/libs/sdklib/.classpath new file mode 100644 index 0000000..fc17a43 --- /dev/null +++ b/sdkmanager/libs/sdklib/.classpath @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/sdkmanager/libs/sdklib/.project b/sdkmanager/libs/sdklib/.project new file mode 100644 index 0000000..97a8578 --- /dev/null +++ b/sdkmanager/libs/sdklib/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>SdkLib</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/sdkmanager/libs/sdklib/Android.mk b/sdkmanager/libs/sdklib/Android.mk new file mode 100644 index 0000000..509c573 --- /dev/null +++ b/sdkmanager/libs/sdklib/Android.mk @@ -0,0 +1,17 @@ +# +# Copyright (C) 2008 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. +# +SDKLIB_LOCAL_DIR := $(call my-dir) +include $(SDKLIB_LOCAL_DIR)/src/Android.mk diff --git a/sdkmanager/libs/sdklib/src/Android.mk b/sdkmanager/libs/sdklib/src/Android.mk new file mode 100644 index 0000000..a059a46 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/Android.mk @@ -0,0 +1,27 @@ +# +# Copyright (C) 2008 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAVA_LIBRARIES := \ + androidprefs + +LOCAL_MODULE := sdklib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/AddOnTarget.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/AddOnTarget.java new file mode 100644 index 0000000..0a59107 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/AddOnTarget.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2008 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; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Represents an add-on target in the SDK. + * An add-on extends a standard {@link PlatformTarget}. + */ +final class AddOnTarget implements IAndroidTarget { + /** + * String to compute hash for add-on targets. + * Format is vendor:name:apiVersion + * */ + private final static String ADD_ON_FORMAT = "%s:%s:%d"; //$NON-NLS-1$ + + private final static class OptionalLibrary implements IOptionalLibrary { + private final String mJarName; + private final String mJarPath; + private final String mName; + private final String mDescription; + + OptionalLibrary(String jarName, String jarPath, String name, String description) { + mJarName = jarName; + mJarPath = jarPath; + mName = name; + mDescription = description; + } + + public String getJarName() { + return mJarName; + } + + public String getJarPath() { + return mJarPath; + } + + public String getName() { + return mName; + } + + public String getDescription() { + return mDescription; + } + } + + private final String mLocation; + private final PlatformTarget mBasePlatform; + private final String mName; + private final String mVendor; + private final String mDescription; + private String[] mSkins; + private String mDefaultSkin; + private IOptionalLibrary[] mLibraries; + + /** + * Creates a new add-on + * @param location the OS path location of the add-on + * @param name the name of the add-on + * @param vendor the vendor name of the add-on + * @param description the add-on description + * @param libMap A map containing the optional libraries. The map key is the fully-qualified + * library name. The value is a 2 string array with the .jar filename, and the description. + * @param basePlatform the platform the add-on is extending. + */ + AddOnTarget(String location, String name, String vendor, String description, + Map<String, String[]> libMap, PlatformTarget basePlatform) { + if (location.endsWith(File.separator) == false) { + location = location + File.separator; + } + + mLocation = location; + mName = name; + mVendor = vendor; + mDescription = description; + mBasePlatform = basePlatform; + + // handle the optional libraries. + if (libMap != null) { + mLibraries = new IOptionalLibrary[libMap.size()]; + int index = 0; + for (Entry<String, String[]> entry : libMap.entrySet()) { + String jarFile = entry.getValue()[0]; + String desc = entry.getValue()[1]; + mLibraries[index++] = new OptionalLibrary(jarFile, + mLocation + SdkConstants.OS_ADDON_LIBS_FOLDER + jarFile, + entry.getKey(), desc); + } + } + } + + public String getLocation() { + return mLocation; + } + + public String getName() { + return mName; + } + + public String getVendor() { + return mVendor; + } + + public String getFullName() { + return String.format("%1$s (%2$s)", mName, mVendor); + } + + public String getDescription() { + return mDescription; + } + + public String getApiVersionName() { + // this is always defined by the base platform + return mBasePlatform.getApiVersionName(); + } + + public int getApiVersionNumber() { + // this is always defined by the base platform + return mBasePlatform.getApiVersionNumber(); + } + + public boolean isPlatform() { + return false; + } + + public IAndroidTarget getParent() { + return mBasePlatform; + } + + public String getPath(int pathId) { + switch (pathId) { + case IMAGES: + return mLocation + SdkConstants.OS_IMAGES_FOLDER; + case SKINS: + return mLocation + SdkConstants.OS_SKINS_FOLDER; + case DOCS: + return mLocation + SdkConstants.FD_DOCS + File.separator; + default : + return mBasePlatform.getPath(pathId); + } + } + + public String[] getSkins() { + return mSkins; + } + + public String getDefaultSkin() { + return mDefaultSkin; + } + + public IOptionalLibrary[] getOptionalLibraries() { + return mLibraries; + } + + public boolean isCompatibleBaseFor(IAndroidTarget target) { + // basic test + if (target == this) { + return true; + } + + // if the receiver has no optional library, then anything with api version number >= to + // the receiver is compatible. + if (mLibraries.length == 0) { + return target.getApiVersionNumber() >= getApiVersionNumber(); + } + + // Otherwise, target is only compatible if the vendor and name are equals with the api + // number greater or equal (ie target is a newer version of this add-on). + if (target.isPlatform() == false) { + return (mVendor.equals(target.getVendor()) && mName.equals(target.getName()) && + target.getApiVersionNumber() >= getApiVersionNumber()); + } + + return false; + } + + public String hashString() { + return String.format(ADD_ON_FORMAT, mVendor, mName, mBasePlatform.getApiVersionNumber()); + } + + @Override + public int hashCode() { + return hashString().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof AddOnTarget) { + AddOnTarget addon = (AddOnTarget)obj; + + return mVendor.equals(addon.mVendor) && mName.equals(addon.mName) && + mBasePlatform.getApiVersionNumber() == addon.mBasePlatform.getApiVersionNumber(); + } + + return super.equals(obj); + } + + /* + * Always return +1 if the object we compare to is a platform. + * Otherwise, do vendor then name then api version comparison. + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + public int compareTo(IAndroidTarget target) { + if (target.isPlatform()) { + return +1; + } + + // vendor + int value = mVendor.compareTo(target.getVendor()); + + // name + if (value == 0) { + value = mName.compareTo(target.getName()); + } + + // api version + if (value == 0) { + value = getApiVersionNumber() - target.getApiVersionNumber(); + } + + return value; + } + + + // ---- local methods. + + + public void setSkins(String[] skins, String defaultSkin) { + mDefaultSkin = defaultSkin; + + // we mix the add-on and base platform skins + HashSet<String> skinSet = new HashSet<String>(); + skinSet.addAll(Arrays.asList(skins)); + skinSet.addAll(Arrays.asList(mBasePlatform.getSkins())); + + mSkins = skinSet.toArray(new String[skinSet.size()]); + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/IAndroidTarget.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/IAndroidTarget.java new file mode 100644 index 0000000..fa462bd --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/IAndroidTarget.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2008 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; + + +/** + * A version of Android that application can target when building. + */ +public interface IAndroidTarget extends Comparable<IAndroidTarget> { + + /** OS Path to the "android.jar" file. */ + public static int ANDROID_JAR = 1; + /** OS Path to the "framework.aidl" file. */ + public static int ANDROID_AIDL = 2; + /** OS Path to "images" folder which contains the emulator system images. */ + public static int IMAGES = 3; + /** OS Path to the "samples" folder which contains sample projects. */ + public static int SAMPLES = 4; + /** OS Path to the "skins" folder which contains the emulator skins. */ + public static int SKINS = 5; + /** OS Path to the "templates" folder which contains the templates for new projects. */ + public static int TEMPLATES = 6; + /** OS Path to the "data" folder which contains data & libraries for the SDK tools. */ + public static int DATA = 7; + /** OS Path to the "attrs.xml" file. */ + public static int ATTRIBUTES = 8; + /** OS Path to the "attrs_manifest.xml" file. */ + public static int MANIFEST_ATTRIBUTES = 9; + /** OS Path to the "data/layoutlib.jar" library. */ + public static int LAYOUT_LIB = 10; + /** OS Path to the "data/res" folder. */ + public static int RESOURCES = 11; + /** OS Path to the "data/fonts" folder. */ + public static int FONTS = 12; + /** OS Path to the "data/widgets.txt" file. */ + public static int WIDGETS = 13; + /** OS Path to the "data/activity_actions.txt" file. */ + public static int ACTIONS_ACTIVITY = 14; + /** OS Path to the "data/broadcast_actions.txt" file. */ + public static int ACTIONS_BROADCAST = 15; + /** OS Path to the "data/service_actions.txt" file. */ + public static int ACTIONS_SERVICE = 16; + /** OS Path to the "data/categories.txt" file. */ + public static int CATEGORIES = 17; + /** OS Path to the "sources" folder. */ + public static int SOURCES = 18; + /** OS Path to the target specific docs */ + public static int DOCS = 19; + /** OS Path to the target's version of the aapt tool. */ + public static int AAPT = 20; + /** OS Path to the target's version of the aidl tool. */ + public static int AIDL = 21; + /** OS Path to the target's version of the dx too. */ + public static int DX = 22; + /** OS Path to the target's version of the dx.jar file. */ + public static int DX_JAR = 23; + + public interface IOptionalLibrary { + String getName(); + String getJarName(); + String getJarPath(); + String getDescription(); + } + + /** + * Returns the target location. + */ + String getLocation(); + + /** + * Returns the name of the vendor of the target. + */ + String getVendor(); + + /** + * Returns the name of the target. + */ + String getName(); + + /** + * Returns the full name of the target, possibly including vendor name. + */ + String getFullName(); + + /** + * Returns the description of the target. + */ + String getDescription(); + + /** + * Returns the api version as an integer. + */ + int getApiVersionNumber(); + + /** + * Returns the platform version as a readable string. + */ + String getApiVersionName(); + + /** + * Returns true if the target is a standard Android platform. + */ + boolean isPlatform(); + + /** + * Returns the parent target. This is likely to only be non <code>null</code> if + * {@link #isPlatform()} returns <code>false</code> + */ + IAndroidTarget getParent(); + + /** + * Returns the path of a platform component. + * @param pathId the id representing the path to return. Any of the constants defined in the + * {@link IAndroidTarget} interface can be used. + */ + String getPath(int pathId); + + /** + * Returns the available skins for this target. + */ + String[] getSkins(); + + /** + * Returns the default skin for this target. + */ + String getDefaultSkin(); + + /** + * Returns the available optional libraries for this target. + * @return an array of optional libraries or <code>null</code> if there is none. + */ + IOptionalLibrary[] getOptionalLibraries(); + + /** + * Returns whether the given target is compatible with the receiver. + * <p/>A target is considered compatible if applications developed for the receiver can run on + * the given target. + * + * @param target the IAndroidTarget to test. + */ + boolean isCompatibleBaseFor(IAndroidTarget target); + + /** + * Returns a string able to uniquely identify a target. + * Typically the target will encode information such as api level, whether it's a platform + * or add-on, and if it's an add-on vendor and add-on name. + */ + String hashString(); +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/ISdkLog.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/ISdkLog.java new file mode 100644 index 0000000..4894517 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/ISdkLog.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2008 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; + +import java.util.Formatter; + +/** + * Interface used to display warnings/errors while parsing the SDK content. + */ +public interface ISdkLog { + + /** + * Prints a warning message on stdout. + * <p/> + * Implementations should only display warnings in verbose mode. + * The message should be prefixed with "Warning:". + * + * @param warningFormat is an optional error format. If non-null, it will be printed + * using a {@link Formatter} with the provided arguments. + * @param args provides the arguments for warningFormat. + */ + void warning(String warningFormat, Object... args); + + /** + * Prints an error message on stderr. + * <p/> + * Implementation should always display errors, independent of verbose mode. + * The message should be prefixed with "Error:". + * + * @param t is an optional {@link Throwable} or {@link Exception}. If non-null, it's + * message will be printed out. + * @param errorFormat is an optional error format. If non-null, it will be printed + * using a {@link Formatter} with the provided arguments. + * @param args provides the arguments for errorFormat. + */ + void error(Throwable t, String errorFormat, Object... args); + + /** + * Prints a message as-is on stdout. + * <p/> + * Implementation should always display errors, independent of verbose mode. + * No prefix is used, the message is printed as-is after formatting. + * + * @param msgFormat is an optional error format. If non-null, it will be printed + * using a {@link Formatter} with the provided arguments. + * @param args provides the arguments for msgFormat. + */ + void printf(String msgFormat, Object... args); +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java new file mode 100644 index 0000000..a3da70e --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2008 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; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a platform target in the SDK. + */ +final class PlatformTarget implements IAndroidTarget { + /** String used to get a hash to the platform target */ + private final static String PLATFORM_HASH = "android-%d"; + + private final static String PLATFORM_VENDOR = "Android"; + private final static String PLATFORM_NAME = "Android %s"; + + private final String mLocation; + private final String mName; + private final int mApiVersionNumber; + private final String mApiVersionName; + private final Map<String, String> mProperties; + private final Map<Integer, String> mPaths = new HashMap<Integer, String>(); + private String[] mSkins; + + PlatformTarget(String location, Map<String, String> properties, + int apiNumber, String apiName) { + mName = String.format(PLATFORM_NAME, apiName); + if (location.endsWith(File.separator) == false) { + location = location + File.separator; + } + mLocation = location; + mProperties = Collections.unmodifiableMap(properties); + mApiVersionNumber = apiNumber; + mApiVersionName = apiName; + + // pre-build the path to the platform components + mPaths.put(ANDROID_JAR, mLocation + SdkConstants.FN_FRAMEWORK_LIBRARY); + mPaths.put(SOURCES, mLocation + SdkConstants.FD_ANDROID_SOURCES); + mPaths.put(ANDROID_AIDL, mLocation + SdkConstants.FN_FRAMEWORK_AIDL); + mPaths.put(IMAGES, mLocation + SdkConstants.OS_IMAGES_FOLDER); + mPaths.put(SAMPLES, mLocation + SdkConstants.OS_PLATFORM_SAMPLES_FOLDER); + mPaths.put(SKINS, mLocation + SdkConstants.OS_SKINS_FOLDER); + mPaths.put(TEMPLATES, mLocation + SdkConstants.OS_PLATFORM_TEMPLATES_FOLDER); + mPaths.put(DATA, mLocation + SdkConstants.OS_PLATFORM_DATA_FOLDER); + mPaths.put(ATTRIBUTES, mLocation + SdkConstants.OS_PLATFORM_ATTRS_XML); + mPaths.put(MANIFEST_ATTRIBUTES, mLocation + SdkConstants.OS_PLATFORM_ATTRS_MANIFEST_XML); + mPaths.put(RESOURCES, mLocation + SdkConstants.OS_PLATFORM_RESOURCES_FOLDER); + mPaths.put(FONTS, mLocation + SdkConstants.OS_PLATFORM_FONTS_FOLDER); + mPaths.put(LAYOUT_LIB, mLocation + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_LAYOUTLIB_JAR); + mPaths.put(WIDGETS, mLocation + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_WIDGETS); + mPaths.put(ACTIONS_ACTIVITY, mLocation + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_INTENT_ACTIONS_ACTIVITY); + mPaths.put(ACTIONS_BROADCAST, mLocation + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_INTENT_ACTIONS_BROADCAST); + mPaths.put(ACTIONS_SERVICE, mLocation + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_INTENT_ACTIONS_SERVICE); + mPaths.put(CATEGORIES, mLocation + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_INTENT_CATEGORIES); + mPaths.put(AAPT, mLocation + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AAPT); + mPaths.put(AIDL, mLocation + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AIDL); + mPaths.put(DX, mLocation + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_DX); + mPaths.put(DX_JAR, mLocation + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + + SdkConstants.FN_DX_JAR); + } + + public String getLocation() { + return mLocation; + } + + /* + * (non-Javadoc) + * + * For Platform, the vendor name is always "Android". + * + * @see com.android.sdklib.IAndroidTarget#getVendor() + */ + public String getVendor() { + return PLATFORM_VENDOR; + } + + public String getName() { + return mName; + } + + public String getFullName() { + return mName; + } + + /* + * (non-Javadoc) + * + * Description for the Android platform is dynamically generated. + * + * @see com.android.sdklib.IAndroidTarget#getDescription() + */ + public String getDescription() { + return String.format("Standard Android platform %s", mApiVersionName); + } + + public int getApiVersionNumber(){ + return mApiVersionNumber; + } + + public String getApiVersionName() { + return mApiVersionName; + } + + public boolean isPlatform() { + return true; + } + + public IAndroidTarget getParent() { + return null; + } + + public String getPath(int pathId) { + return mPaths.get(pathId); + } + + public String[] getSkins() { + return mSkins; + } + + public String getDefaultSkin() { + // at this time, this is the default skin for all the platform. + return "HVGA"; + } + + /* + * Always returns null, as a standard platforms have no optional libraries. + * + * (non-Javadoc) + * @see com.android.sdklib.IAndroidTarget#getOptionalLibraries() + */ + public IOptionalLibrary[] getOptionalLibraries() { + return null; + } + + public boolean isCompatibleBaseFor(IAndroidTarget target) { + // basic test + if (target == this) { + return true; + } + + // target is compatible wit the receiver as long as its api version number is greater or + // equal. + return target.getApiVersionNumber() >= mApiVersionNumber; + } + + public String hashString() { + return String.format(PLATFORM_HASH, mApiVersionNumber); + } + + @Override + public int hashCode() { + return hashString().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PlatformTarget) { + return mApiVersionNumber == ((PlatformTarget)obj).mApiVersionNumber; + } + + return super.equals(obj); + } + + /* + * Always return -1 if the object we compare to is an addon. + * Otherwise, compare api level. + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + public int compareTo(IAndroidTarget target) { + if (target.isPlatform() == false) { + return -1; + } + + return mApiVersionNumber - target.getApiVersionNumber(); + } + + // ---- platform only methods. + + public String getProperty(String name) { + return mProperties.get(name); + } + + public Map<String, String> getProperties() { + return mProperties; // mProperties is unmodifiable. + } + + void setSkins(String[] skins) { + mSkins = skins; + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java new file mode 100644 index 0000000..00594d1 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2007 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; + +import java.io.File; + +/** + * Constant definition class.<br> + * <br> + * Most constants have a prefix defining the content. + * <ul> + * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li> + * <li><code>FN_</code> File name constant.</li> + * <li><code>FD_</code> Folder name constant.</li> + * </ul> + * + */ +public final class SdkConstants { + public final static int PLATFORM_UNKNOWN = 0; + public final static int PLATFORM_LINUX = 1; + public final static int PLATFORM_WINDOWS = 2; + public final static int PLATFORM_DARWIN = 3; + + /** + * Returns current platform, one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, + * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. + */ + public final static int CURRENT_PLATFORM = currentPlatform(); + + + /** An SDK Project's AndroidManifest.xml file */ + public static final String FN_ANDROID_MANIFEST_XML= "AndroidManifest.xml"; + /** An SDK Project's build.xml file */ + public final static String FN_BUILD_XML = "build.xml"; + + /** Name of the framework library, i.e. "android.jar" */ + public static final String FN_FRAMEWORK_LIBRARY = "android.jar"; + /** Name of the layout attributes, i.e. "attrs.xml" */ + public static final String FN_ATTRS_XML = "attrs.xml"; + /** Name of the layout attributes, i.e. "attrs_manifest.xml" */ + public static final String FN_ATTRS_MANIFEST_XML = "attrs_manifest.xml"; + /** framework aidl import file */ + public static final String FN_FRAMEWORK_AIDL = "framework.aidl"; + /** layoutlib.jar file */ + public static final String FN_LAYOUTLIB_JAR = "layoutlib.jar"; + /** widget list file */ + public static final String FN_WIDGETS = "widgets.txt"; + /** Intent activity actions list file */ + public static final String FN_INTENT_ACTIONS_ACTIVITY = "activity_actions.txt"; + /** Intent broadcast actions list file */ + public static final String FN_INTENT_ACTIONS_BROADCAST = "broadcast_actions.txt"; + /** Intent service actions list file */ + public static final String FN_INTENT_ACTIONS_SERVICE = "service_actions.txt"; + /** Intent category list file */ + public static final String FN_INTENT_CATEGORIES = "categories.txt"; + + /** platform build property file */ + public final static String FN_BUILD_PROP = "build.prop"; + /** plugin properties file */ + public final static String FN_PLUGIN_PROP = "plugin.prop"; + /** add-on manifest file */ + public final static String FN_MANIFEST_INI = "manifest.ini"; + /** hardware properties definition file */ + public final static String FN_HARDWARE_INI = "hardware-properties.ini"; + + /** Skin layout file */ + public final static String FN_SKIN_LAYOUT = "layout";//$NON-NLS-1$ + + /** dex.jar file */ + public static final String FN_DX_JAR = "dx.jar"; //$NON-NLS-1$ + + /** dx executable */ + public final static String FN_DX = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "dx.bat" : "dx"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** aapt executable */ + public final static String FN_AAPT = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "aapt.exe" : "aapt"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** aidl executable */ + public final static String FN_AIDL = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "aidl.exe" : "aidl"; //$NON-NLS-1$ //$NON-NLS-2$ + + /* Folder Names for Android Projects . */ + + /** Resources folder name, i.e. "res". */ + public final static String FD_RESOURCES = "res"; //$NON-NLS-1$ + /** Assets folder name, i.e. "assets" */ + public final static String FD_ASSETS = "assets"; //$NON-NLS-1$ + /** Default source folder name, i.e. "src" */ + public final static String FD_SOURCES = "src"; //$NON-NLS-1$ + /** Default generated source folder name, i.e. "gen" */ + public final static String FD_GEN_SOURCES = "gen"; //$NON-NLS-1$ + /** Default native library folder name inside the project, i.e. "libs" + * While the folder inside the .apk is "lib", we call that one libs because + * that's what we use in ant for both .jar and .so and we need to make the 2 development ways + * compatible. */ + public final static String FD_NATIVE_LIBS = "libs"; //$NON-NLS-1$ + /** Native lib folder inside the APK: "lib" */ + public final static String FD_APK_NATIVE_LIBS = "lib"; //$NON-NLS-1$ + /** Default output folder name, i.e. "bin" */ + public final static String FD_OUTPUT = "bin"; //$NON-NLS-1$ + /** Default anim resource folder name, i.e. "anim" */ + public final static String FD_ANIM = "anim"; //$NON-NLS-1$ + /** Default color resource folder name, i.e. "color" */ + public final static String FD_COLOR = "color"; //$NON-NLS-1$ + /** Default drawable resource folder name, i.e. "drawable" */ + public final static String FD_DRAWABLE = "drawable"; //$NON-NLS-1$ + /** Default layout resource folder name, i.e. "layout" */ + public final static String FD_LAYOUT = "layout"; //$NON-NLS-1$ + /** Default menu resource folder name, i.e. "menu" */ + public final static String FD_MENU = "menu"; //$NON-NLS-1$ + /** Default values resource folder name, i.e. "values" */ + public final static String FD_VALUES = "values"; //$NON-NLS-1$ + /** Default xml resource folder name, i.e. "xml" */ + public final static String FD_XML = "xml"; //$NON-NLS-1$ + /** Default raw resource folder name, i.e. "raw" */ + public final static String FD_RAW = "raw"; //$NON-NLS-1$ + + /* Folder Names for the Android SDK */ + + /** Name of the SDK platforms folder. */ + public final static String FD_PLATFORMS = "platforms"; + /** Name of the SDK addons folder. */ + public final static String FD_ADDONS = "add-ons"; + /** Name of the SDK tools folder. */ + public final static String FD_TOOLS = "tools"; + /** Name of the SDK tools/lib folder. */ + public final static String FD_LIB = "lib"; + /** Name of the SDK docs folder. */ + public final static String FD_DOCS = "docs"; + /** Name of the SDK images folder. */ + public final static String FD_IMAGES = "images"; + /** Name of the SDK skins folder. */ + public final static String FD_SKINS = "skins"; + /** Name of the SDK samples folder. */ + public final static String FD_SAMPLES = "samples"; + /** Name of the SDK templates folder, i.e. "templates" */ + public final static String FD_TEMPLATES = "templates"; + /** Name of the SDK data folder, i.e. "data" */ + public final static String FD_DATA = "data"; + /** Name of the SDK resources folder, i.e. "res" */ + public final static String FD_RES = "res"; + /** Name of the SDK font folder, i.e. "fonts" */ + public final static String FD_FONTS = "fonts"; + /** Name of the android sources directory */ + public static final String FD_ANDROID_SOURCES = "sources"; + /** Name of the addon libs folder. */ + public final static String FD_ADDON_LIBS = "libs"; + + /** Namespace for the resource XML, i.e. "http://schemas.android.com/apk/res/android" */ + public final static String NS_RESOURCES = "http://schemas.android.com/apk/res/android"; + + /* Folder path relative to the SDK root */ + /** Path of the documentation directory relative to the sdk folder. + * This is an OS path, ending with a separator. */ + public final static String OS_SDK_DOCS_FOLDER = FD_DOCS + File.separator; + + /** Path of the tools directory relative to the sdk folder, or to a platform folder. + * This is an OS path, ending with a separator. */ + public final static String OS_SDK_TOOLS_FOLDER = FD_TOOLS + File.separator; + + /** Path of the lib directory relative to the sdk folder, or to a platform folder. + * This is an OS path, ending with a separator. */ + public final static String OS_SDK_TOOLS_LIB_FOLDER = + OS_SDK_TOOLS_FOLDER + FD_LIB + File.separator; + + /* Folder paths relative to a platform or add-on folder */ + + /** Path of the images directory relative to a platform or addon folder. + * This is an OS path, ending with a separator. */ + public final static String OS_IMAGES_FOLDER = FD_IMAGES + File.separator; + + /** Path of the skin directory relative to a platform or addon folder. + * This is an OS path, ending with a separator. */ + public final static String OS_SKINS_FOLDER = FD_SKINS + File.separator; + + /* Folder paths relative to a Platform folder */ + + /** Path of the data directory relative to a platform folder. + * This is an OS path, ending with a separator. */ + public final static String OS_PLATFORM_DATA_FOLDER = FD_DATA + File.separator; + + /** Path of the samples directory relative to a platform folder. + * This is an OS path, ending with a separator. */ + public final static String OS_PLATFORM_SAMPLES_FOLDER = FD_SAMPLES + File.separator; + + /** Path of the resources directory relative to a platform folder. + * This is an OS path, ending with a separator. */ + public final static String OS_PLATFORM_RESOURCES_FOLDER = + OS_PLATFORM_DATA_FOLDER + FD_RES + File.separator; + + /** Path of the fonts directory relative to a platform folder. + * This is an OS path, ending with a separator. */ + public final static String OS_PLATFORM_FONTS_FOLDER = + OS_PLATFORM_DATA_FOLDER + FD_FONTS + File.separator; + + /** Path of the android source directory relative to a platform folder. + * This is an OS path, ending with a separator. */ + public final static String OS_PLATFORM_SOURCES_FOLDER = FD_ANDROID_SOURCES + File.separator; + + /** Path of the android templates directory relative to a platform folder. + * This is an OS path, ending with a separator. */ + public final static String OS_PLATFORM_TEMPLATES_FOLDER = FD_TEMPLATES + File.separator; + + /** Path of the attrs.xml file relative to a platform folder. */ + public final static String OS_PLATFORM_ATTRS_XML = + OS_PLATFORM_RESOURCES_FOLDER + FD_VALUES + File.separator + FN_ATTRS_XML; + + /** Path of the attrs_manifest.xml file relative to a platform folder. */ + public final static String OS_PLATFORM_ATTRS_MANIFEST_XML = + OS_PLATFORM_RESOURCES_FOLDER + FD_VALUES + File.separator + FN_ATTRS_MANIFEST_XML; + + /** Path of the layoutlib.jar file relative to a platform folder. */ + public final static String OS_PLATFORM_LAYOUTLIB_JAR = + OS_PLATFORM_DATA_FOLDER + FN_LAYOUTLIB_JAR; + + /* Folder paths relative to a addon folder */ + + /** Path of the images directory relative to a folder folder. + * This is an OS path, ending with a separator. */ + public final static String OS_ADDON_LIBS_FOLDER = FD_ADDON_LIBS + File.separator; + + + /** Skin default **/ + public final static String SKIN_DEFAULT = "default"; + + /** Returns the appropriate name for the 'android' command, which is 'android.bat' for + * Windows and 'android' for all other platforms. */ + public static String androidCmdName() { + String os = System.getProperty("os.name"); + String cmd = "android"; + if (os.startsWith("Windows")) { + cmd += ".bat"; + } + return cmd; + } + + /** Returns the appropriate name for the 'mksdcard' command, which is 'mksdcard.exe' for + * Windows and 'mkdsdcard' for all other platforms. */ + public static String mkSdCardCmdName() { + String os = System.getProperty("os.name"); + String cmd = "mksdcard"; + if (os.startsWith("Windows")) { + cmd += ".exe"; + } + return cmd; + } + + /** + * Returns current platform + * + * @return one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, + * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. + */ + private static int currentPlatform() { + String os = System.getProperty("os.name"); //$NON-NLS-1$ + if (os.startsWith("Mac OS")) { //$NON-NLS-1$ + return PLATFORM_DARWIN; + } else if (os.startsWith("Windows")) { //$NON-NLS-1$ + return PLATFORM_WINDOWS; + } else if (os.startsWith("Linux")) { //$NON-NLS-1$ + return PLATFORM_LINUX; + } + + return PLATFORM_UNKNOWN; + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java new file mode 100644 index 0000000..28227c6 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java @@ -0,0 +1,511 @@ +/* + * Copyright (C) 2008 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; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The SDK manager parses the SDK folder and gives access to the content. + * @see PlatformTarget + * @see AddOnTarget + */ +public final class SdkManager { + + public final static String PROP_VERSION_SDK = "ro.build.version.sdk"; + public final static String PROP_VERSION_RELEASE = "ro.build.version.release"; + + private final static String ADDON_NAME = "name"; + private final static String ADDON_VENDOR = "vendor"; + private final static String ADDON_API = "api"; + private final static String ADDON_DESCRIPTION = "description"; + private final static String ADDON_LIBRARIES = "libraries"; + private final static String ADDON_DEFAULT_SKIN = "skin"; + + private final static Pattern PATTERN_PROP = Pattern.compile( + "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); + + private final static Pattern PATTERN_LIB_DATA = Pattern.compile( + "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE); + + /** List of items in the platform to check when parsing it. These paths are relative to the + * platform root folder. */ + private final static String[] sPlatformContentList = new String[] { + SdkConstants.FN_FRAMEWORK_LIBRARY, + SdkConstants.FN_FRAMEWORK_AIDL, + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AAPT, + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AIDL, + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_DX, + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR, + }; + + /** the location of the SDK */ + private final String mSdkLocation; + private IAndroidTarget[] mTargets; + + /** + * Creates an {@link SdkManager} for a given sdk location. + * @param sdkLocation the location of the SDK. + * @param log the ISdkLog object receiving warning/error from the parsing. + * @return the created {@link SdkManager} or null if the location is not valid. + */ + public static SdkManager createManager(String sdkLocation, ISdkLog log) { + try { + SdkManager manager = new SdkManager(sdkLocation); + ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>(); + manager.loadPlatforms(list, log); + manager.loadAddOns(list, log); + + // sort the targets/add-ons + Collections.sort(list); + + manager.setTargets(list.toArray(new IAndroidTarget[list.size()])); + + return manager; + } catch (IllegalArgumentException e) { + if (log != null) { + log.error(e, "Error parsing the sdk."); + } + } + + return null; + } + + /** + * Returns the location of the SDK. + */ + public String getLocation() { + return mSdkLocation; + } + + /** + * Returns the targets that are available in the SDK. + */ + public IAndroidTarget[] getTargets() { + return mTargets; + } + + /** + * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. + * + * @param hash the {@link IAndroidTarget} hash string. + * @return The matching {@link IAndroidTarget} or null. + */ + public IAndroidTarget getTargetFromHashString(String hash) { + if (hash != null) { + for (IAndroidTarget target : mTargets) { + if (hash.equals(target.hashString())) { + return target; + } + } + } + + return null; + } + + + private SdkManager(String sdkLocation) { + mSdkLocation = sdkLocation; + } + + private void setTargets(IAndroidTarget[] targets) { + mTargets = targets; + } + + /** + * Loads the Platforms from the SDK. + * @param list the list to fill with the platforms. + * @param log the ISdkLog object receiving warning/error from the parsing. + */ + private void loadPlatforms(ArrayList<IAndroidTarget> list, ISdkLog log) { + File platformFolder = new File(mSdkLocation, SdkConstants.FD_PLATFORMS); + if (platformFolder.isDirectory()) { + File[] platforms = platformFolder.listFiles(); + + for (File platform : platforms) { + if (platform.isDirectory()) { + PlatformTarget target = loadPlatform(platform, log); + if (target != null) { + list.add(target); + } + } else if (log != null) { + log.warning("Ignoring platform '%1$s', not a folder.", platform.getName()); + } + } + + return; + } + + String message = null; + if (platformFolder.exists() == false) { + message = "%s is missing."; + } else { + message = "%s is not a folder."; + } + + throw new IllegalArgumentException(String.format(message, + platformFolder.getAbsolutePath())); + } + + /** + * Loads a specific Platform at a given location. + * @param platform the location of the platform. + * @param log the ISdkLog object receiving warning/error from the parsing. + */ + private PlatformTarget loadPlatform(File platform, ISdkLog log) { + File buildProp = new File(platform, SdkConstants.FN_BUILD_PROP); + + if (buildProp.isFile()) { + Map<String, String> map = parsePropertyFile(buildProp, log); + + if (map != null) { + // look for some specific values in the map. + try { + String apiNumber = map.get(PROP_VERSION_SDK); + String apiName = map.get(PROP_VERSION_RELEASE); + if (apiNumber != null && apiName != null) { + // api number and name looks valid, perform a few more checks + if (checkPlatformContent(platform, log) == false) { + return null; + } + // create the target. + PlatformTarget target = new PlatformTarget( + platform.getAbsolutePath(), + map, + Integer.parseInt(apiNumber), + apiName); + + // need to parse the skins. + String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS)); + target.setSkins(skins); + + return target; + } + } catch (NumberFormatException e) { + // looks like apiNumber does not parse to a number. + // Ignore this platform. + if (log != null) { + log.error(null, + "Ignoring platform '%1$s': %2$s is not a valid number in %3$s.", + platform.getName(), PROP_VERSION_SDK, SdkConstants.FN_BUILD_PROP); + } + } + } + } else if (log != null) { + log.error(null, "Ignoring platform '%1$s': %2$s is missing.", platform.getName(), + SdkConstants.FN_BUILD_PROP); + } + + return null; + } + + /** + * Loads the Add-on from the SDK. + * @param list the list to fill with the add-ons. + * @param log the ISdkLog object receiving warning/error from the parsing. + */ + private void loadAddOns(ArrayList<IAndroidTarget> list, ISdkLog log) { + File addonFolder = new File(mSdkLocation, SdkConstants.FD_ADDONS); + if (addonFolder.isDirectory()) { + File[] addons = addonFolder.listFiles(); + + for (File addon : addons) { + // Add-ons have to be folders. Ignore files and no need to warn about them. + if (addon.isDirectory()) { + AddOnTarget target = loadAddon(addon, list, log); + if (target != null) { + list.add(target); + } + } + } + + return; + } + + String message = null; + if (addonFolder.exists() == false) { + message = "%s is missing."; + } else { + message = "%s is not a folder."; + } + + throw new IllegalArgumentException(String.format(message, + addonFolder.getAbsolutePath())); + } + + /** + * Loads a specific Add-on at a given location. + * @param addon the location of the addon. + * @param list + * @param log + */ + private AddOnTarget loadAddon(File addon, ArrayList<IAndroidTarget> list, ISdkLog log) { + File addOnManifest = new File(addon, SdkConstants.FN_MANIFEST_INI); + + if (addOnManifest.isFile()) { + Map<String, String> propertyMap = parsePropertyFile(addOnManifest, log); + + if (propertyMap != null) { + // look for some specific values in the map. + // we require name, vendor, and api + String name = propertyMap.get(ADDON_NAME); + if (name == null) { + displayAddonManifestError(log, addon.getName(), ADDON_NAME); + return null; + } + + String vendor = propertyMap.get(ADDON_VENDOR); + if (vendor == null) { + displayAddonManifestError(log, addon.getName(), ADDON_VENDOR); + return null; + } + + String api = propertyMap.get(ADDON_API); + PlatformTarget baseTarget = null; + if (api == null) { + displayAddonManifestError(log, addon.getName(), ADDON_API); + return null; + } else { + try { + int apiValue = Integer.parseInt(api); + for (IAndroidTarget target : list) { + if (target.isPlatform() && + target.getApiVersionNumber() == apiValue) { + baseTarget = (PlatformTarget)target; + break; + } + } + + if (baseTarget == null) { + if (log != null) { + log.error(null, + "Ignoring add-on '%1$s': Unable to find base platform with API level %2$d", + addon.getName(), apiValue); + } + + return null; + } + } catch (NumberFormatException e) { + // looks like apiNumber does not parse to a number. + // Ignore this add-on. + if (log != null) { + log.error(null, + "Ignoring add-on '%1$s': %2$s is not a valid number in %3$s.", + addon.getName(), ADDON_API, SdkConstants.FN_BUILD_PROP); + } + return null; + } + } + + // get the optional description + String description = propertyMap.get(ADDON_DESCRIPTION); + + // get the optional libraries + String librariesValue = propertyMap.get(ADDON_LIBRARIES); + Map<String, String[]> libMap = null; + + if (librariesValue != null) { + librariesValue = librariesValue.trim(); + if (librariesValue.length() > 0) { + // split in the string into the libraries name + String[] libraries = librariesValue.split(";"); + if (libraries.length > 0) { + libMap = new HashMap<String, String[]>(); + for (String libName : libraries) { + libName = libName.trim(); + + // get the library data from the properties + String libData = propertyMap.get(libName); + + if (libData != null) { + // split the jar file from the description + Matcher m = PATTERN_LIB_DATA.matcher(libData); + if (m.matches()) { + libMap.put(libName, new String[] { + m.group(1), m.group(2) }); + } else if (log != null) { + log.error(null, + "Ignoring library '%1$s', property value has wrong format\n\t%2$s", + libName, libData); + } + } else if (log != null) { + log.error(null, + "Ignoring library '%1$s', missing property value", + libName, libData); + } + } + } + } + } + + AddOnTarget target = new AddOnTarget(addon.getAbsolutePath(), name, vendor, + description, libMap, baseTarget); + + // need to parse the skins. + String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS)); + + // get the default skin, or take it from the base platform if needed. + String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN); + + if (defaultSkin == null) { + if (skins.length == 1) { + defaultSkin = skins[1]; + } else { + defaultSkin = baseTarget.getDefaultSkin(); + } + } + + target.setSkins(skins, defaultSkin); + + return target; + } + } else if (log != null) { + log.error(null, "Ignoring add-on '%1$s': %2$s is missing.", addon.getName(), + SdkConstants.FN_MANIFEST_INI); + } + + return null; + } + + private void displayAddonManifestError(ISdkLog log, String addonName, String valueName) { + if (log != null) { + log.error(null, "Ignoring add-on '%1$s': '%2$s' is missing from %3$s.", + addonName, valueName, SdkConstants.FN_MANIFEST_INI); + } + } + + /** + * Checks the given platform has all the required files, and returns true if they are all + * present. + * <p/>This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe), + * aidl(.exe), dx(.bat), and dx.jar + */ + private boolean checkPlatformContent(File platform, ISdkLog log) { + for (String relativePath : sPlatformContentList) { + File f = new File(platform, relativePath); + if (f.exists() == false) { + log.error(null, + "Ignoring platform '%1$s': %2$s is missing.", + platform.getName(), relativePath); + return false; + } + + } + return true; + } + + + /** + * Parses a property file and returns + * @param buildProp the property file to parse + * @param log the ISdkLog object receiving warning/error from the parsing. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + public static Map<String, String> parsePropertyFile(File buildProp, ISdkLog log) { + FileInputStream fis = null; + BufferedReader reader = null; + try { + fis = new FileInputStream(buildProp); + reader = new BufferedReader(new InputStreamReader(fis)); + + String line = null; + Map<String, String> map = new HashMap<String, String>(); + while ((line = reader.readLine()) != null) { + if (line.length() > 0 && line.charAt(0) != '#') { + + Matcher m = PATTERN_PROP.matcher(line); + if (m.matches()) { + map.put(m.group(1), m.group(2)); + } else { + log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", + buildProp.getAbsolutePath(), 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.", buildProp.getAbsolutePath(), + e.getMessage()); + } + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // pass + } + } + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + // pass + } + } + } + + return null; + } + + /** + * Parses the skin folder and builds the skin list. + * @param osPath The path of the skin root folder. + */ + private String[] parseSkinFolder(String osPath) { + File skinRootFolder = new File(osPath); + + if (skinRootFolder.isDirectory()) { + ArrayList<String> skinList = new ArrayList<String>(); + + File[] files = skinRootFolder.listFiles(); + + for (File skinFolder : files) { + if (skinFolder.isDirectory()) { + // check for layout file + File layout = new File(skinFolder, SdkConstants.FN_SKIN_LAYOUT); + + if (layout.isFile()) { + // for now we don't parse the content of the layout and + // simply add the directory to the list. + skinList.add(skinFolder.getName()); + } + } + } + + return skinList.toArray(new String[skinList.size()]); + } + + return new String[0]; + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java new file mode 100644 index 0000000..0ea89d1 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java @@ -0,0 +1,800 @@ +/* + * Copyright (C) 2008 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.avd; + +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISdkLog; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.SdkManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Android Virtual Device Manager to manage AVDs. + */ +public final class AvdManager { + + public static final String AVD_FOLDER_EXTENSION = ".avd"; + + private final static String AVD_INFO_PATH = "path"; + private final static String AVD_INFO_TARGET = "target"; + + public final static String AVD_INI_SKIN_PATH = "skin.path"; + public final static String AVD_INI_SKIN_NAME = "skin.name"; + public final static String AVD_INI_SDCARD_PATH = "sdcard.path"; + public final static String AVD_INI_SDCARD_SIZE = "sdcard.size"; + public final static String AVD_INI_IMAGES_1 = "image.sysdir.1"; + public final static String AVD_INI_IMAGES_2 = "image.sysdir.2"; + + /** + * Pattern to match pixel-sized skin "names", e.g. "320x480". + */ + public final static Pattern NUMERIC_SKIN_SIZE = Pattern.compile("[0-9]{2,}x[0-9]{2,}"); + + + private final static String USERDATA_IMG = "userdata.img"; + private final static String CONFIG_INI = "config.ini"; + private final static String SDCARD_IMG = "sdcard.img"; + + private final static String INI_EXTENSION = ".ini"; + private final static Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + INI_EXTENSION + "$", + Pattern.CASE_INSENSITIVE); + + private final static Pattern SDCARD_SIZE_PATTERN = Pattern.compile("\\d+[MK]?"); + + /** An immutable structure describing an Android Virtual Device. */ + public static final class AvdInfo { + private final String mName; + private final String mPath; + private final IAndroidTarget mTarget; + private final Map<String, String> mProperties; + + /** Creates a new AVD info. Values are immutable. + * @param properties */ + public AvdInfo(String name, String path, IAndroidTarget target, + Map<String, String> properties) { + mName = name; + mPath = path; + mTarget = target; + mProperties = properties; + } + + /** Returns the name of the AVD. */ + public String getName() { + return mName; + } + + /** Returns the path of the AVD data directory. */ + public String getPath() { + return mPath; + } + + /** Returns the target of the AVD. */ + public IAndroidTarget getTarget() { + return mTarget; + } + + /** + * Helper method that returns the .ini {@link File} for a given AVD name. + * @throws AndroidLocationException if there's a problem getting android root directory. + */ + public static File getIniFile(String name) throws AndroidLocationException { + String avdRoot; + avdRoot = AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD; + return new File(avdRoot, name + INI_EXTENSION); + } + + /** + * Returns the .ini {@link File} for this AVD. + * @throws AndroidLocationException if there's a problem getting android root directory. + */ + public File getIniFile() throws AndroidLocationException { + return getIniFile(mName); + } + + /** + * Returns a map of properties for the AVD. + */ + public Map<String, String> getProperties() { + return mProperties; + } + } + + private final ArrayList<AvdInfo> mAvdList = new ArrayList<AvdInfo>(); + private ISdkLog mSdkLog; + private final SdkManager mSdk; + + public AvdManager(SdkManager sdk, ISdkLog sdkLog) throws AndroidLocationException { + mSdk = sdk; + mSdkLog = sdkLog; + buildAvdList(); + } + + /** + * Returns the existing AVDs. + * @return a newly allocated array containing all the AVDs. + */ + public AvdInfo[] getAvds() { + return mAvdList.toArray(new AvdInfo[mAvdList.size()]); + } + + /** + * Returns the {@link AvdInfo} matching the given <var>name</var>. + * @return the matching AvdInfo or <code>null</code> if none were found. + */ + public AvdInfo getAvd(String name) { + for (AvdInfo info : mAvdList) { + if (info.getName().equals(name)) { + return info; + } + } + + return null; + } + + /** + * Creates a new AVD. It is expected that there is no existing AVD with this name already. + * @param avdFolder the data folder for the AVD. It will be created as needed. + * @param name the name of the AVD + * @param target the target of the AVD + * @param skinName the name of the skin. Can be null. Must have been verified by caller. + * @param sdcard the parameter value for the sdCard. Can be null. This is either a path to + * an existing sdcard image or a sdcard size (\d+, \d+K, \dM). + * @param hardwareConfig the hardware setup for the AVD + * @param removePrevious If true remove any previous files. + */ + public AvdInfo createAvd(File avdFolder, String name, IAndroidTarget target, + String skinName, String sdcard, Map<String,String> hardwareConfig, + boolean removePrevious, ISdkLog log) { + + File iniFile = null; + boolean needCleanup = false; + try { + if (avdFolder.exists()) { + if (removePrevious) { + // AVD already exists and removePrevious is set, try to remove the + // directory's content first (but not the directory itself). + recursiveDelete(avdFolder); + } else { + // AVD shouldn't already exist if removePrevious is false. + if (log != null) { + log.error(null, + "Folder %s is in the way. Use --force if you want to overwrite.", + avdFolder.getAbsolutePath()); + } + return null; + } + } else { + // create the AVD folder. + avdFolder.mkdir(); + } + + // actually write the ini file + iniFile = createAvdIniFile(name, avdFolder, target); + + // writes the userdata.img in it. + String imagePath = target.getPath(IAndroidTarget.IMAGES); + File userdataSrc = new File(imagePath, USERDATA_IMG); + + if (userdataSrc.exists() == false && target.isPlatform() == false) { + imagePath = target.getParent().getPath(IAndroidTarget.IMAGES); + userdataSrc = new File(imagePath, USERDATA_IMG); + } + + if (userdataSrc.exists() == false) { + log.error(null, "Unable to find a '%1$s' file to copy into the AVD folder.", + USERDATA_IMG); + needCleanup = true; + return null; + } + + FileInputStream fis = new FileInputStream(userdataSrc); + + File userdataDest = new File(avdFolder, USERDATA_IMG); + FileOutputStream fos = new FileOutputStream(userdataDest); + + byte[] buffer = new byte[4096]; + int count; + while ((count = fis.read(buffer)) != -1) { + fos.write(buffer, 0, count); + } + + fos.close(); + fis.close(); + + // Config file. + HashMap<String, String> values = new HashMap<String, String>(); + + // First the image folders of the target itself + imagePath = getImageRelativePath(target, log); + if (imagePath == null) { + needCleanup = true; + return null; + } + + values.put(AVD_INI_IMAGES_1, imagePath); + + // If the target is an add-on we need to add the Platform image as a backup. + IAndroidTarget parent = target.getParent(); + if (parent != null) { + imagePath = getImageRelativePath(parent, log); + if (imagePath == null) { + needCleanup = true; + return null; + } + + values.put(AVD_INI_IMAGES_2, imagePath); + } + + + // Now the skin. + if (skinName == null) { + skinName = target.getDefaultSkin(); + } + + if (NUMERIC_SKIN_SIZE.matcher(skinName).matches()) { + // Skin name is an actual screen resolution, no skin.path + values.put(AVD_INI_SKIN_NAME, skinName); + } else { + // get the path of the skin (relative to the SDK) + // assume skin name is valid + String skinPath = getSkinRelativePath(skinName, target, log); + if (skinPath == null) { + needCleanup = true; + return null; + } + + values.put(AVD_INI_SKIN_PATH, skinPath); + values.put(AVD_INI_SKIN_NAME, skinName); + } + + if (sdcard != null) { + File sdcardFile = new File(sdcard); + if (sdcardFile.isFile()) { + // sdcard value is an external sdcard, so we put its path into the config.ini + values.put(AVD_INI_SDCARD_PATH, sdcard); + } else { + // Sdcard is possibly a size. In that case we create a file called 'sdcard.img' + // in the AVD folder, and do not put any value in config.ini. + + // First, check that it matches the pattern for sdcard size + Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard); + if (m.matches()) { + // create the sdcard. + sdcardFile = new File(avdFolder, SDCARD_IMG); + String path = sdcardFile.getAbsolutePath(); + + // execute mksdcard with the proper parameters. + File toolsFolder = new File(mSdk.getLocation(), SdkConstants.FD_TOOLS); + File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName()); + + if (mkSdCard.isFile() == false) { + log.error(null, "'%1$s' is missing from the SDK tools folder.", + mkSdCard.getName()); + needCleanup = true; + return null; + } + + if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) { + needCleanup = true; + return null; // mksdcard output has already been displayed, no need to + // output anything else. + } + + // add a property containing the size of the sdcard for display purpose + // only when the dev does 'android list avd' + values.put(AVD_INI_SDCARD_SIZE, sdcard); + } else { + log.error(null, + "'%1$s' is not recognized as a valid sdcard value.\n" + + "Value should be:\n" + + "1. path to an sdcard.\n" + + "2. size of the sdcard to create: <size>[K|M]", + sdcard); + needCleanup = true; + return null; + } + } + } + + if (hardwareConfig != null) { + values.putAll(hardwareConfig); + } + + File configIniFile = new File(avdFolder, CONFIG_INI); + createConfigIni(configIniFile, values); + + if (log != null) { + if (target.isPlatform()) { + log.printf("Created AVD '%s' based on %s\n", name, target.getName()); + } else { + log.printf("Created AVD '%s' based on %s (%s)\n", name, target.getName(), + target.getVendor()); + } + } + + // create the AvdInfo object, and add it to the list + AvdInfo avdInfo = new AvdInfo(name, avdFolder.getAbsolutePath(), target, values); + + mAvdList.add(avdInfo); + + return avdInfo; + } catch (AndroidLocationException e) { + if (log != null) { + log.error(e, null); + } + } catch (IOException e) { + if (log != null) { + log.error(e, null); + } + } finally { + if (needCleanup) { + if (iniFile != null && iniFile.exists()) { + iniFile.delete(); + } + + recursiveDelete(avdFolder); + avdFolder.delete(); + } + } + + return null; + } + + /** + * Returns the path to the target images folder as a relative path to the SDK. + */ + private String getImageRelativePath(IAndroidTarget target, ISdkLog log) { + String imageFullPath = target.getPath(IAndroidTarget.IMAGES); + + // make this path relative to the SDK location + String sdkLocation = mSdk.getLocation(); + if (imageFullPath.startsWith(sdkLocation) == false) { + // this really really should not happen. + log.error(null, "Target location is not inside the SDK."); + assert false; + return null; + } + + imageFullPath = imageFullPath.substring(sdkLocation.length()); + if (imageFullPath.charAt(0) == File.separatorChar) { + imageFullPath = imageFullPath.substring(1); + } + return imageFullPath; + } + + /** + * Returns the path to the skin, as a relative path to the SDK. + */ + private String getSkinRelativePath(String skinName, IAndroidTarget target, ISdkLog log) { + // first look to see if the skin is in the target + + String path = target.getPath(IAndroidTarget.SKINS); + File skin = new File(path, skinName); + + if (skin.exists() == false && target.isPlatform() == false) { + target = target.getParent(); + + path = target.getPath(IAndroidTarget.SKINS); + skin = new File(path, skinName); + } + + // skin really does not exist! + if (skin.exists() == false) { + log.error(null, "Skin '%1$s' does not exist.", skinName); + return null; + } + + // get the skin path + path = skin.getAbsolutePath(); + + // make this path relative to the SDK location + String sdkLocation = mSdk.getLocation(); + if (path.startsWith(sdkLocation) == false) { + // this really really should not happen. + log.error(null, "Target location is not inside the SDK."); + assert false; + return null; + } + + path = path.substring(sdkLocation.length()); + if (path.charAt(0) == File.separatorChar) { + path = path.substring(1); + } + return path; + } + + /** + * Creates the ini file for an AVD. + * + * @param name of the AVD. + * @param avdFolder path for the data folder of the AVD. + * @param target of the AVD. + * @throws AndroidLocationException if there's a problem getting android root directory. + * @throws IOException if {@link File#getAbsolutePath()} fails. + */ + private File createAvdIniFile(String name, File avdFolder, IAndroidTarget target) + throws AndroidLocationException, IOException { + HashMap<String, String> values = new HashMap<String, String>(); + File iniFile = AvdInfo.getIniFile(name); + values.put(AVD_INFO_PATH, avdFolder.getAbsolutePath()); + values.put(AVD_INFO_TARGET, target.hashString()); + createConfigIni(iniFile, values); + + return iniFile; + } + + /** + * Creates the ini file for an AVD. + * + * @param info of the AVD. + * @throws AndroidLocationException if there's a problem getting android root directory. + * @throws IOException if {@link File#getAbsolutePath()} fails. + */ + private File createAvdIniFile(AvdInfo info) throws AndroidLocationException, IOException { + return createAvdIniFile(info.getName(), new File(info.getPath()), info.getTarget()); + } + + /** + * Actually deletes the files of an existing AVD. + * <p/> + * This also remove it from the manager's list, The caller does not need to + * call {@link #removeAvd(AvdInfo)} afterwards. + * + * @param avdInfo the information on the AVD to delete + */ + public void deleteAvd(AvdInfo avdInfo, ISdkLog log) { + try { + File f = avdInfo.getIniFile(); + if (f.exists()) { + log.warning("Deleting file %s", f.getCanonicalPath()); + if (!f.delete()) { + log.error(null, "Failed to delete %s", f.getCanonicalPath()); + } + } + + f = new File(avdInfo.getPath()); + if (f.exists()) { + log.warning("Deleting folder %s", f.getCanonicalPath()); + recursiveDelete(f); + if (!f.delete()) { + log.error(null, "Failed to delete %s", f.getCanonicalPath()); + } + } + + removeAvd(avdInfo); + } catch (AndroidLocationException e) { + log.error(e, null); + } catch (IOException e) { + log.error(e, null); + } + } + + /** + * Moves and/or rename an existing AVD and its files. + * This also change it in the manager's list. + * <p/> + * The caller should make sure the name or path given are valid, do not exist and are + * actually different than current values. + * + * @param avdInfo the information on the AVD to move. + * @param newName the new name of the AVD if non null. + * @param paramFolderPath the new data folder if non null. + * @return True if the move succeeded or there was nothing to do. + * If false, this method will have had already output error in the log. + */ + public boolean moveAvd(AvdInfo avdInfo, String newName, String paramFolderPath, ISdkLog log) { + + try { + if (paramFolderPath != null) { + File f = new File(avdInfo.getPath()); + log.warning("Moving '%s' to '%s'.", avdInfo.getPath(), paramFolderPath); + if (!f.renameTo(new File(paramFolderPath))) { + log.error(null, "Failed to move '%s' to '%s'.", + avdInfo.getPath(), paramFolderPath); + return false; + } + + // update avd info + AvdInfo info = new AvdInfo(avdInfo.getName(), paramFolderPath, avdInfo.getTarget(), + avdInfo.getProperties()); + mAvdList.remove(avdInfo); + mAvdList.add(info); + avdInfo = info; + + // update the ini file + createAvdIniFile(avdInfo); + } + + if (newName != null) { + File oldIniFile = avdInfo.getIniFile(); + File newIniFile = AvdInfo.getIniFile(newName); + + log.warning("Moving '%s' to '%s'.", oldIniFile.getPath(), newIniFile.getPath()); + if (!oldIniFile.renameTo(newIniFile)) { + log.error(null, "Failed to move '%s' to '%s'.", + oldIniFile.getPath(), newIniFile.getPath()); + return false; + } + + // update avd info + AvdInfo info = new AvdInfo(newName, avdInfo.getPath(), avdInfo.getTarget(), + avdInfo.getProperties()); + mAvdList.remove(avdInfo); + mAvdList.add(info); + } + } catch (AndroidLocationException e) { + log.error(e, null); + } catch (IOException e) { + log.error(e, null); + } + + // nothing to do or succeeded + return true; + } + + /** + * Helper method to recursively delete a folder's content (but not the folder itself). + * + * @throws SecurityException like {@link File#delete()} does if file/folder is not writable. + */ + public void recursiveDelete(File folder) { + for (File f : folder.listFiles()) { + if (f.isDirectory()) { + recursiveDelete(folder); + } + f.delete(); + } + } + + private void buildAvdList() throws AndroidLocationException { + // get the Android prefs location. + String avdRoot = AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD; + + final boolean avdListDebug = System.getenv("AVD_LIST_DEBUG") != null; + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] AVD root: '%s'\n", avdRoot); + } + + // ensure folder validity. + File folder = new File(avdRoot); + if (folder.isFile()) { + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] AVD root is a file.\n"); + } + throw new AndroidLocationException(String.format("%s is not a valid folder.", avdRoot)); + } else if (folder.exists() == false) { + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] AVD root folder doesn't exist, creating.\n"); + } + // folder is not there, we create it and return + folder.mkdirs(); + return; + } + + File[] avds = folder.listFiles(new FilenameFilter() { + public boolean accept(File parent, String name) { + if (INI_NAME_PATTERN.matcher(name).matches()) { + // check it's a file and not a folder + boolean isFile = new File(parent, name).isFile(); + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] Item '%s': %s\n", + name, isFile ? "accepted file" : "rejected"); + } + return isFile; + } + + return false; + } + }); + + for (File avd : avds) { + AvdInfo info = parseAvdInfo(avd); + if (info != null) { + mAvdList.add(info); + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] Added AVD '%s'\n", info.getPath()); + } + } else if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] Failed to parse AVD '%s'\n", avd.getPath()); + } + } + } + + private AvdInfo parseAvdInfo(File path) { + Map<String, String> map = SdkManager.parsePropertyFile(path, mSdkLog); + + String avdPath = map.get(AVD_INFO_PATH); + if (avdPath == null) { + return null; + } + + String targetHash = map.get(AVD_INFO_TARGET); + if (targetHash == null) { + return null; + } + + IAndroidTarget target = mSdk.getTargetFromHashString(targetHash); + if (target == null) { + return null; + } + + // load the avd properties. + File configIniFile = new File(avdPath, CONFIG_INI); + Map<String, String> properties = SdkManager.parsePropertyFile(configIniFile, mSdkLog); + + Matcher matcher = INI_NAME_PATTERN.matcher(path.getName()); + + AvdInfo info = new AvdInfo( + matcher.matches() ? matcher.group(1) : path.getName(), // should not happen + avdPath, + target, + properties); + + return info; + } + + private static void createConfigIni(File iniFile, Map<String, String> values) + throws IOException { + FileWriter writer = new FileWriter(iniFile); + + for (Entry<String, String> entry : values.entrySet()) { + writer.write(String.format("%s=%s\n", entry.getKey(), entry.getValue())); + } + writer.close(); + + } + + private boolean createSdCard(String toolLocation, String size, String location, ISdkLog log) { + try { + String[] command = new String[3]; + command[0] = toolLocation; + command[1] = size; + command[2] = location; + Process process = Runtime.getRuntime().exec(command); + + ArrayList<String> errorOutput = new ArrayList<String>(); + ArrayList<String> stdOutput = new ArrayList<String>(); + int status = grabProcessOutput(process, errorOutput, stdOutput, + true /* waitForReaders */); + + if (status != 0) { + log.error(null, "Failed to create the SD card."); + for (String error : errorOutput) { + log.error(null, error); + } + + return false; + } + + return true; + } catch (InterruptedException e) { + log.error(null, "Failed to create the SD card."); + } catch (IOException e) { + log.error(null, "Failed to create the SD card."); + } + + return false; + } + + /** + * Gets the stderr/stdout outputs of a process and returns when the process is done. + * Both <b>must</b> be read or the process will block on windows. + * @param process The process to get the ouput from + * @param errorOutput The array to store the stderr output. cannot be null. + * @param stdOutput The array to store the stdout output. cannot be null. + * @param waitforReaders if true, this will wait for the reader threads. + * @return the process return code. + * @throws InterruptedException + */ + private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput, + final ArrayList<String> stdOutput, boolean waitforReaders) + throws InterruptedException { + assert errorOutput != null; + assert stdOutput != null; + // read the lines as they come. if null is returned, it's + // because the process finished + Thread t1 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + errorOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + Thread t2 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + stdOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + t1.start(); + t2.start(); + + // it looks like on windows process#waitFor() can return + // before the thread have filled the arrays, so we wait for both threads and the + // process itself. + if (waitforReaders) { + try { + t1.join(); + } catch (InterruptedException e) { + } + try { + t2.join(); + } catch (InterruptedException e) { + } + } + + // get the return code from the process + return process.waitFor(); + } + + /** + * Removes an {@link AvdInfo} from the internal list. + * + * @param avdInfo The {@link AvdInfo} to remove. + * @return true if this {@link AvdInfo} was present and has been removed. + */ + public boolean removeAvd(AvdInfo avdInfo) { + return mAvdList.remove(avdInfo); + } + +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/HardwareProperties.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/HardwareProperties.java new file mode 100644 index 0000000..ed5b949 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/HardwareProperties.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2008 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.avd; + +import com.android.sdklib.ISdkLog; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HardwareProperties { + private final static Pattern PATTERN_PROP = Pattern.compile( + "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); + + private final static String HW_PROP_NAME = "name"; + private final static String HW_PROP_TYPE = "type"; + private final static String HW_PROP_DEFAULT = "default"; + private final static String HW_PROP_ABSTRACT = "abstract"; + private final static String HW_PROP_DESC = "description"; + + public enum ValueType { + INTEGER("integer"), + BOOLEAN("boolean"), + DISKSIZE("diskSize"); + + private String mValue; + + ValueType(String value) { + mValue = value; + } + + public static ValueType getEnum(String value) { + for (ValueType type : values()) { + if (type.mValue.equals(value)) { + return type; + } + } + + return null; + } + } + + public static final class HardwareProperty { + private String mName; + private ValueType mType; + /** the string representation of the default value. can be null. */ + private String mDefault; + private String mAbstract; + private String mDescription; + + public String getName() { + return mName; + } + + public ValueType getType() { + return mType; + } + + public String getDefault() { + return mDefault; + } + + public String getAbstract() { + return mAbstract; + } + + public String getDescription() { + return mDescription; + } + } + + /** + * Parses the hardware definition file. + * @param file the property file to parse + * @param log the ISdkLog object receiving warning/error from the parsing. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + public static List<HardwareProperty> parseHardwareDefinitions(File file, ISdkLog log) { + try { + FileInputStream fis = new FileInputStream(file); + BufferedReader reader = new BufferedReader(new InputStreamReader(fis)); + + List<HardwareProperty> map = new ArrayList<HardwareProperty>(); + + String line = null; + HardwareProperty prop = null; + while ((line = reader.readLine()) != null) { + if (line.length() > 0 && line.charAt(0) != '#') { + Matcher m = PATTERN_PROP.matcher(line); + if (m.matches()) { + String valueName = m.group(1); + String value = m.group(2); + + if (HW_PROP_NAME.equals(valueName)) { + prop = new HardwareProperty(); + prop.mName = value; + map.add(prop); + } + + if (prop == null) { + log.warning("Error parsing '%1$s': missing '%2$s'", + file.getAbsolutePath(), HW_PROP_NAME); + return null; + } + + if (HW_PROP_TYPE.equals(valueName)) { + prop.mType = ValueType.getEnum(value); + } else if (HW_PROP_DEFAULT.equals(valueName)) { + prop.mDefault = value; + } else if (HW_PROP_ABSTRACT.equals(valueName)) { + prop.mAbstract = value; + } else if (HW_PROP_DESC.equals(valueName)) { + prop.mDescription = value; + } + } else { + log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", + file.getAbsolutePath(), 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.", file.getAbsolutePath(), + e.getMessage()); + } + } + + return null; + } + +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ApkConfigurationHelper.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ApkConfigurationHelper.java new file mode 100644 index 0000000..b89d3bd --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ApkConfigurationHelper.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2009 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.project; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +/** + * Helper class to read and write Apk Configuration into a {@link ProjectProperties} file. + */ +public class ApkConfigurationHelper { + /** Prefix for property names for config definition. This prevents having config named + * after other valid properties such as "target". */ + final static String CONFIG_PREFIX = "apk-config-"; + + /** + * Reads the Apk Configurations from a {@link ProjectProperties} file and returns them as a map. + * <p/>If there are no defined configurations, the returned map will be empty. + * @return a map of apk configurations. The map contains (name, filter) where name is + * the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of + * resource configuration to include in the apk (see aapt -c) + */ + public static Map<String, String> getConfigs(ProjectProperties properties) { + HashMap<String, String> configMap = new HashMap<String, String>(); + + // get the list of configs. + String configList = properties.getProperty(ProjectProperties.PROPERTY_APK_CONFIGS); + if (configList != null) { + // this is a comma separated list + String[] configs = configList.split(","); //$NON-NLS-1$ + + // read the value of each config and store it in a map + for (String config : configs) { + config = config.trim(); + String configValue = properties.getProperty(CONFIG_PREFIX + config); + if (configValue != null) { + configMap.put(config, configValue); + } + } + } + + return configMap; + } + + /** + * Writes the Apk Configurations from a given map into a {@link ProjectProperties}. + * @param properties the {@link ProjectProperties} in which to store the apk configurations. + * @param configMap a map of apk configurations. The map contains (name, filter) where name is + * the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of + * resource configuration to include in the apk (see aapt -c) + * @return true if the {@link ProjectProperties} contained Apk Configuration that were not + * present in the map. + */ + public static boolean setConfigs(ProjectProperties properties, Map<String, String> configMap) { + // load the current configs, in order to remove the value properties for each of them + // in case a config was removed. + + // get the list of configs. + String configList = properties.getProperty(ProjectProperties.PROPERTY_APK_CONFIGS); + + boolean hasRemovedConfig = false; + + if (configList != null) { + // this is a comma separated list + String[] configs = configList.split(","); //$NON-NLS-1$ + + for (String config : configs) { + config = config.trim(); + if (configMap.containsKey(config) == false) { + hasRemovedConfig = true; + properties.removeProperty(CONFIG_PREFIX + config); + } + } + } + + // now add the properties. + Set<Entry<String, String>> entrySet = configMap.entrySet(); + StringBuilder sb = new StringBuilder(); + for (Entry<String, String> entry : entrySet) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(entry.getKey()); + properties.setProperty(CONFIG_PREFIX + entry.getKey(), entry.getValue()); + } + properties.setProperty(ProjectProperties.PROPERTY_APK_CONFIGS, sb.toString()); + + return hasRemovedConfig; + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java new file mode 100644 index 0000000..7489b65 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java @@ -0,0 +1,742 @@ +/* + * Copyright (C) 2007 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.project; + +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISdkLog; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.project.ProjectProperties.PropertyType; + +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +/** + * Creates the basic files needed to get an Android project up and running. Also + * allows creation of IntelliJ project files. + * + * @hide + */ +public class ProjectCreator { + + /** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */ + private final static String PH_JAVA_FOLDER = "PACKAGE_PATH"; + /** Package name substitution string used in template files, i.e. "PACKAGE" */ + private final static String PH_PACKAGE = "PACKAGE"; + /** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME". */ + private final static String PH_ACTIVITY_NAME = "ACTIVITY_NAME"; + /** Project name substitution string used in template files, i.e. "PROJECT_NAME". */ + private final static String PH_PROJECT_NAME = "PROJECT_NAME"; + + private final static String FOLDER_TESTS = "tests"; + + public enum OutputLevel { + /** Silent mode. Project creation will only display errors. */ + SILENT, + /** Normal mode. Project creation will display what's being done, display + * error but not warnings. */ + NORMAL, + /** Verbose mode. Project creation will display what's being done, errors and warnings. */ + VERBOSE; + } + + /** + * Exception thrown when a project creation fails, typically because a template + * file cannot be written. + */ + private static class ProjectCreateException extends Exception { + /** default UID. This will not be serialized anyway. */ + private static final long serialVersionUID = 1L; + + ProjectCreateException(String message) { + super(message); + } + + ProjectCreateException(Throwable t, String format, Object... args) { + super(format != null ? String.format(format, args) : format, t); + } + + ProjectCreateException(String format, Object... args) { + super(String.format(format, args)); + } + } + + private final OutputLevel mLevel; + + private final ISdkLog mLog; + private final String mSdkFolder; + + public ProjectCreator(String sdkFolder, OutputLevel level, ISdkLog log) { + mSdkFolder = sdkFolder; + mLevel = level; + mLog = log; + } + + /** + * Creates a new project. + * + * @param folderPath the folder of the project to create. + * @param projectName the name of the project. + * @param packageName the package of the project. + * @param activityName the activity of the project as it will appear in the manifest. + * @param target the project target. + * @param isTestProject whether the project to create is a test project. + */ + public void createProject(String folderPath, String projectName, + String packageName, String activityName, IAndroidTarget target, + boolean isTestProject) { + + // create project folder if it does not exist + File projectFolder = new File(folderPath); + if (!projectFolder.exists()) { + + boolean created = false; + Throwable t = null; + try { + created = projectFolder.mkdirs(); + } catch (Exception e) { + t = e; + } + + if (created) { + println("Created project directory: %1$s", projectFolder); + } else { + mLog.error(t, "Could not create directory: %1$s", projectFolder); + return; + } + } else { + Exception e = null; + String error = null; + try { + String[] content = projectFolder.list(); + if (content == null) { + error = "Project folder '%1$s' is not a directory."; + } else if (content.length != 0) { + error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead."; + } + } catch (Exception e1) { + e = e1; + } + + if (e != null || error != null) { + mLog.error(e, error, projectFolder, SdkConstants.androidCmdName()); + } + } + + try { + // first create the project properties. + + // location of the SDK goes in localProperty + ProjectProperties localProperties = ProjectProperties.create(folderPath, + PropertyType.LOCAL); + localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); + localProperties.save(); + + // target goes in default properties + ProjectProperties defaultProperties = ProjectProperties.create(folderPath, + PropertyType.DEFAULT); + defaultProperties.setAndroidTarget(target); + defaultProperties.save(); + + // create an empty build.properties + ProjectProperties buildProperties = ProjectProperties.create(folderPath, + PropertyType.BUILD); + buildProperties.save(); + + // create the map for place-holders of values to replace in the templates + final HashMap<String, String> keywords = new HashMap<String, String>(); + + // create the required folders. + // compute src folder path + final String packagePath = + stripString(packageName.replace(".", File.separator), + File.separatorChar); + + // put this path in the place-holder map for project files that needs to list + // files manually. + keywords.put(PH_JAVA_FOLDER, packagePath); + + keywords.put(PH_PACKAGE, packageName); + if (activityName != null) { + keywords.put(PH_ACTIVITY_NAME, activityName); + } + + // Take the project name from the command line if there's one + if (projectName != null) { + keywords.put(PH_PROJECT_NAME, projectName); + } else { + if (activityName != null) { + // Use the activity as project name + keywords.put(PH_PROJECT_NAME, activityName); + } else { + // We need a project name. Just pick up the basename of the project + // directory. + projectName = projectFolder.getName(); + keywords.put(PH_PROJECT_NAME, projectName); + } + } + + // create the source folder and the java package folders. + String srcFolderPath = SdkConstants.FD_SOURCES + File.separator + packagePath; + File sourceFolder = createDirs(projectFolder, srcFolderPath); + String javaTemplate = "java_file.template"; + String activityFileName = activityName + ".java"; + if (isTestProject) { + javaTemplate = "java_tests_file.template"; + activityFileName = activityName + "Test.java"; + } + installTemplate(javaTemplate, new File(sourceFolder, activityFileName), + keywords, target); + + // create the generate source folder + srcFolderPath = SdkConstants.FD_GEN_SOURCES + File.separator + packagePath; + sourceFolder = createDirs(projectFolder, srcFolderPath); + + // create other useful folders + File resourceFodler = createDirs(projectFolder, SdkConstants.FD_RESOURCES); + createDirs(projectFolder, SdkConstants.FD_OUTPUT); + createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS); + + if (isTestProject == false) { + /* Make res files only for non test projects */ + File valueFolder = createDirs(resourceFodler, SdkConstants.FD_VALUES); + installTemplate("strings.template", new File(valueFolder, "strings.xml"), + keywords, target); + + File layoutFolder = createDirs(resourceFodler, SdkConstants.FD_LAYOUT); + installTemplate("layout.template", new File(layoutFolder, "main.xml"), + keywords, target); + } + + /* Make AndroidManifest.xml and build.xml files */ + String manifestTemplate = "AndroidManifest.template"; + if (isTestProject) { + manifestTemplate = "AndroidManifest.tests.template"; + } + + installTemplate(manifestTemplate, + new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML), + keywords, target); + + installTemplate("build.template", + new File(projectFolder, SdkConstants.FN_BUILD_XML), + keywords); + + // if this is not a test project, then we create one. + if (isTestProject == false) { + // create the test project folder. + createDirs(projectFolder, FOLDER_TESTS); + File testProjectFolder = new File(folderPath, FOLDER_TESTS); + + createProject(testProjectFolder.getAbsolutePath(), projectName, packageName, + activityName, target, true /*isTestProject*/); + } + } catch (ProjectCreateException e) { + mLog.error(e, null); + } catch (IOException e) { + mLog.error(e, null); + } + } + + /** + * Updates an existing project. + * <p/> + * Workflow: + * <ul> + * <li> Check AndroidManifest.xml is present (required) + * <li> Check there's a default.properties with a target *or* --target was specified + * <li> Update default.prop if --target was specified + * <li> Refresh/create "sdk" in local.properties + * <li> Build.xml: create if not present or no <androidinit(\w|/>) in it + * </ul> + * + * @param folderPath the folder of the project to update. This folder must exist. + * @param target the project target. Can be null. + * @param projectName The project name from --name. Can be null. + */ + public void updateProject(String folderPath, IAndroidTarget target, String projectName ) { + // project folder must exist and be a directory, since this is an update + File projectFolder = new File(folderPath); + if (!projectFolder.isDirectory()) { + mLog.error(null, "Project folder '%1$s' is not a valid directory, this is not an Android project you can update.", + projectFolder); + return; + } + + // Check AndroidManifest.xml is present + File androidManifest = new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML); + if (!androidManifest.isFile()) { + mLog.error(null, + "%1$s not found in '%2$s', this is not an Android project you can update.", + SdkConstants.FN_ANDROID_MANIFEST_XML, + folderPath); + return; + } + + // Check there's a default.properties with a target *or* --target was specified + ProjectProperties props = ProjectProperties.load(folderPath, PropertyType.DEFAULT); + if (props == null || props.getProperty(ProjectProperties.PROPERTY_TARGET) == null) { + if (target == null) { + mLog.error(null, + "There is no %1$s file in '%2$s'. Please provide a --target to the '%3$s update' command.", + PropertyType.DEFAULT.getFilename(), + folderPath, + SdkConstants.androidCmdName()); + return; + } + } + + // Update default.prop if --target was specified + if (target != null) { + // we already attempted to load the file earlier, if that failed, create it. + if (props == null) { + props = ProjectProperties.create(folderPath, PropertyType.DEFAULT); + } + + // set or replace the target + props.setAndroidTarget(target); + try { + props.save(); + println("Updated %1$s", PropertyType.DEFAULT.getFilename()); + } catch (IOException e) { + mLog.error(e, "Failed to write %1$s file in '%2$s'", + PropertyType.DEFAULT.getFilename(), + folderPath); + return; + } + } + + // Refresh/create "sdk" in local.properties + // because the file may already exists and contain other values (like apk config), + // we first try to load it. + props = ProjectProperties.load(folderPath, PropertyType.LOCAL); + if (props == null) { + props = ProjectProperties.create(folderPath, PropertyType.LOCAL); + } + + // set or replace the sdk location. + props.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); + try { + props.save(); + println("Updated %1$s", PropertyType.LOCAL.getFilename()); + } catch (IOException e) { + mLog.error(e, "Failed to write %1$s file in '%2$s'", + PropertyType.LOCAL.getFilename(), + folderPath); + return; + } + + // Build.xml: create if not present or no <androidinit/> in it + File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML); + boolean needsBuildXml = projectName != null || !buildXml.exists(); + if (!needsBuildXml) { + // Note that "<androidinit" must be followed by either a whitespace, a "/" (for the + // XML /> closing tag) or an end-of-line. This way we know the XML tag is really this + // one and later we will be able to use an "androidinit2" tag or such as necessary. + needsBuildXml = !checkFileContainsRegexp(buildXml, "<androidinit(?:\\s|/|$)"); + if (needsBuildXml) { + println("File %1$s is too old and needs to be updated.", SdkConstants.FN_BUILD_XML); + } + } + + if (needsBuildXml) { + // create the map for place-holders of values to replace in the templates + final HashMap<String, String> keywords = new HashMap<String, String>(); + + // Take the project name from the command line if there's one + if (projectName != null) { + keywords.put(PH_PROJECT_NAME, projectName); + } else { + extractPackageFromManifest(androidManifest, keywords); + if (keywords.containsKey(PH_ACTIVITY_NAME)) { + // Use the activity as project name + keywords.put(PH_PROJECT_NAME, keywords.get(PH_ACTIVITY_NAME)); + } else { + // We need a project name. Just pick up the basename of the project + // directory. + projectName = projectFolder.getName(); + keywords.put(PH_PROJECT_NAME, projectName); + } + } + + if (mLevel == OutputLevel.VERBOSE) { + println("Regenerating %1$s with project name %2$s", + SdkConstants.FN_BUILD_XML, + keywords.get(PH_PROJECT_NAME)); + } + + try { + installTemplate("build.template", + new File(projectFolder, SdkConstants.FN_BUILD_XML), + keywords); + } catch (ProjectCreateException e) { + mLog.error(e, null); + } + } + } + + /** + * Returns true if any line of the input file contains the requested regexp. + */ + private boolean checkFileContainsRegexp(File file, String regexp) { + Pattern p = Pattern.compile(regexp); + + try { + BufferedReader in = new BufferedReader(new FileReader(file)); + String line; + + while ((line = in.readLine()) != null) { + if (p.matcher(line).find()) { + return true; + } + } + + in.close(); + } catch (Exception e) { + // ignore + } + + return false; + } + + /** + * Extracts a "full" package & activity name from an AndroidManifest.xml. + * <p/> + * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}. + * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_NAME}. + * When no activity is found, this key is not created. + * + * @param manifestFile The AndroidManifest.xml file + * @param outKeywords Place where to put the out parameters: package and activity names. + * @return True if the package/activity was parsed and updated in the keyword dictionary. + */ + private boolean extractPackageFromManifest(File manifestFile, + Map<String, String> outKeywords) { + try { + final String nsPrefix = "android"; + final String nsURI = SdkConstants.NS_RESOURCES; + + XPath xpath = XPathFactory.newInstance().newXPath(); + + xpath.setNamespaceContext(new NamespaceContext() { + public String getNamespaceURI(String prefix) { + if (nsPrefix.equals(prefix)) { + return nsURI; + } + return XMLConstants.NULL_NS_URI; + } + + public String getPrefix(String namespaceURI) { + if (nsURI.equals(namespaceURI)) { + return nsPrefix; + } + return null; + } + + @SuppressWarnings("unchecked") + public Iterator getPrefixes(String namespaceURI) { + if (nsURI.equals(namespaceURI)) { + ArrayList<String> list = new ArrayList<String>(); + list.add(nsPrefix); + return list.iterator(); + } + return null; + } + + }); + + InputSource source = new InputSource(new FileReader(manifestFile)); + String packageName = xpath.evaluate("/manifest/@package", source); + + source = new InputSource(new FileReader(manifestFile)); + + // Select the "android:name" attribute of all <activity> nodes but only if they + // contain a sub-node <intent-filter><action> with an "android:name" attribute which + // is 'android.intent.action.MAIN' and an <intent-filter><category> with an + // "android:name" attribute which is 'android.intent.category.LAUNCHER' + String expression = String.format("/manifest/application/activity" + + "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " + + "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" + + "/@%1$s:name", nsPrefix); + + NodeList activityNames = (NodeList) xpath.evaluate(expression, source, + XPathConstants.NODESET); + + // If we get here, both XPath expressions were valid so we're most likely dealing + // with an actual AndroidManifest.xml file. The nodes may not have the requested + // attributes though, if which case we should warn. + + if (packageName == null || packageName.length() == 0) { + mLog.error(null, + "Missing <manifest package=\"...\"> in '%1$s'", + manifestFile.getName()); + return false; + } + + // Get the first activity that matched earlier. If there is no activity, + // activityName is set to an empty string and the generated "combined" name + // will be in the form "package." (with a dot at the end). + String activityName = ""; + if (activityNames.getLength() > 0) { + activityName = activityNames.item(0).getNodeValue(); + } + + if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) { + println("WARNING: There is more than one activity defined in '%1$s'.\n" + + "Only the first one will be used. If this is not appropriate, you need\n" + + "to specify one of these values manually instead:", + manifestFile.getName()); + + for (int i = 0; i < activityNames.getLength(); i++) { + String name = activityNames.item(i).getNodeValue(); + name = combinePackageActivityNames(packageName, name); + println("- %1$s", name); + } + } + + if (activityName.length() == 0) { + mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" + + "No activity will be generated.", + nsPrefix, manifestFile.getName()); + } else { + outKeywords.put(PH_ACTIVITY_NAME, activityName); + } + + outKeywords.put(PH_PACKAGE, packageName); + return true; + + } catch (IOException e) { + mLog.error(e, "Failed to read %1$s", manifestFile.getName()); + } catch (XPathExpressionException e) { + Throwable t = e.getCause(); + mLog.error(t == null ? e : t, + "Failed to parse %1$s", + manifestFile.getName()); + } + + return false; + } + + private String combinePackageActivityNames(String packageName, String activityName) { + // Activity Name can have 3 forms: + // - ".Name" means this is a class name in the given package name. + // The full FQCN is thus packageName + ".Name" + // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name" + // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is. + // To be valid, the package name should have at least two components. This is checked + // later during the creation of the build.xml file, so we just need to detect there's + // a dot but not at pos==0. + + int pos = activityName.indexOf('.'); + if (pos == 0) { + return packageName + activityName; + } else if (pos > 0) { + return activityName; + } else { + return packageName + "." + activityName; + } + } + + /** + * Installs a new file that is based on a template file provided by a given target. + * Each match of each key from the place-holder map in the template will be replaced with its + * corresponding value in the created file. + * + * @param templateName the name of to the template file + * @param destFile the path to the destination file, relative to the project + * @param placeholderMap a map of (place-holder, value) to create the file from the template. + * @param target the Target of the project that will be providing the template. + * @throws ProjectCreateException + */ + private void installTemplate(String templateName, File destFile, + Map<String, String> placeholderMap, IAndroidTarget target) + throws ProjectCreateException { + // query the target for its template directory + String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); + final String sourcePath = templateFolder + File.separator + templateName; + + installFullPathTemplate(sourcePath, destFile, placeholderMap); + } + + /** + * Installs a new file that is based on a template file provided by the tools folder. + * Each match of each key from the place-holder map in the template will be replaced with its + * corresponding value in the created file. + * + * @param templateName the name of to the template file + * @param destFile the path to the destination file, relative to the project + * @param placeholderMap a map of (place-holder, value) to create the file from the template. + * @throws ProjectCreateException + */ + private void installTemplate(String templateName, File destFile, + Map<String, String> placeholderMap) + throws ProjectCreateException { + // query the target for its template directory + String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; + final String sourcePath = templateFolder + File.separator + templateName; + + installFullPathTemplate(sourcePath, destFile, placeholderMap); + } + + /** + * Installs a new file that is based on a template. + * Each match of each key from the place-holder map in the template will be replaced with its + * corresponding value in the created file. + * + * @param sourcePath the full path to the source template file + * @param destFile the destination file + * @param placeholderMap a map of (place-holder, value) to create the file from the template. + * @throws ProjectCreateException + */ + private void installFullPathTemplate(String sourcePath, File destFile, + Map<String, String> placeholderMap) throws ProjectCreateException { + + boolean existed = destFile.exists(); + + try { + BufferedWriter out = new BufferedWriter(new FileWriter(destFile)); + BufferedReader in = new BufferedReader(new FileReader(sourcePath)); + String line; + + while ((line = in.readLine()) != null) { + for (String key : placeholderMap.keySet()) { + line = line.replace(key, placeholderMap.get(key)); + } + + out.write(line); + out.newLine(); + } + + out.close(); + in.close(); + } catch (Exception e) { + throw new ProjectCreateException(e, "Could not access %1$s: %2$s", + destFile, e.getMessage()); + } + + println("%1$s file %2$s", + existed ? "Updated" : "Added", + destFile); + } + + /** + * Prints a message unless silence is enabled. + * <p/> + * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from + * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}. + * + * @param format Format for String.format + * @param args Arguments for String.format + */ + private void println(String format, Object... args) { + if (mLevel != OutputLevel.SILENT) { + if (!format.endsWith("\n")) { + format += "\n"; + } + mLog.printf(format, args); + } + } + + /** + * Creates a new folder, along with any parent folders that do not exists. + * + * @param parent the parent folder + * @param name the name of the directory to create. + * @throws ProjectCreateException + */ + private File createDirs(File parent, String name) throws ProjectCreateException { + final File newFolder = new File(parent, name); + boolean existedBefore = true; + + if (!newFolder.exists()) { + if (!newFolder.mkdirs()) { + throw new ProjectCreateException("Could not create directory: %1$s", newFolder); + } + existedBefore = false; + } + + if (newFolder.isDirectory()) { + if (!newFolder.canWrite()) { + throw new ProjectCreateException("Path is not writable: %1$s", newFolder); + } + } else { + throw new ProjectCreateException("Path is not a directory: %1$s", newFolder); + } + + if (!existedBefore) { + try { + println("Created directory %1$s", newFolder.getCanonicalPath()); + } catch (IOException e) { + throw new ProjectCreateException( + "Could not determine canonical path of created directory", e); + } + } + + return newFolder; + } + + /** + * Strips the string of beginning and trailing characters (multiple + * characters will be stripped, example stripString("..test...", '.') + * results in "test"; + * + * @param s the string to strip + * @param strip the character to strip from beginning and end + * @return the stripped string or the empty string if everything is stripped. + */ + private static String stripString(String s, char strip) { + final int sLen = s.length(); + int newStart = 0, newEnd = sLen - 1; + + while (newStart < sLen && s.charAt(newStart) == strip) { + newStart++; + } + while (newEnd >= 0 && s.charAt(newEnd) == strip) { + newEnd--; + } + + /* + * newEnd contains a char we want, and substring takes end as being + * exclusive + */ + newEnd++; + + if (newStart >= sLen || newEnd < 0) { + return ""; + } + + return s.substring(newStart, newEnd); + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectProperties.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectProperties.java new file mode 100644 index 0000000..69a16be --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectProperties.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2008 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.project; + +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Class to load and save project properties for both ADT and Ant-based build. + * + */ +public final class ProjectProperties { + /** The property name for the project target */ + public final static String PROPERTY_TARGET = "target"; + public final static String PROPERTY_APK_CONFIGS = "apk-configurations"; + public final static String PROPERTY_SDK = "sdk-location"; + + public static enum PropertyType { + BUILD("build.properties", BUILD_HEADER), + DEFAULT("default.properties", DEFAULT_HEADER), + LOCAL("local.properties", LOCAL_HEADER); + + private final String mFilename; + private final String mHeader; + + PropertyType(String filename, String header) { + mFilename = filename; + mHeader = header; + } + + public String getFilename() { + return mFilename; + } + } + + private final static String LOCAL_HEADER = +// 1-------10--------20--------30--------40--------50--------60--------70--------80 + "# This file is automatically generated by Android Tools.\n" + + "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" + + "# \n" + + "# This file must *NOT* be checked in Version Control Systems,\n" + + "# as it contains information specific to your local configuration.\n" + + "\n"; + + private final static String DEFAULT_HEADER = +// 1-------10--------20--------30--------40--------50--------60--------70--------80 + "# This file is automatically generated by Android Tools.\n" + + "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" + + "# \n" + + "# This file must be checked in Version Control Systems.\n" + + "# \n" + + "# To customize properties used by the Ant build system use,\n" + + "# \"build.properties\", and override values to adapt the script to your\n" + + "# project structure.\n" + + "\n"; + + private final static String BUILD_HEADER = +// 1-------10--------20--------30--------40--------50--------60--------70--------80 + "# This file is used to override default values used by the Ant build system.\n" + + "# \n" + + "# This file must be checked in Version Control Systems, as it is\n" + + "# integral to the build system of your project.\n" + + "\n" + + "# The name of your application package as defined in the manifest.\n" + + "# Used by the 'uninstall' rule.\n"+ + "#application-package=com.example.myproject\n" + + "\n" + + "# The name of the source folder.\n" + + "#source-folder=src\n" + + "\n" + + "# The name of the output folder.\n" + + "#out-folder=bin\n" + + "\n"; + + private final static Map<String, String> COMMENT_MAP = new HashMap<String, String>(); + static { +// 1-------10--------20--------30--------40--------50--------60--------70--------80 + COMMENT_MAP.put(PROPERTY_TARGET, + "# Project target.\n"); + COMMENT_MAP.put(PROPERTY_APK_CONFIGS, + "# apk configurations. This property allows creation of APK files with limited\n" + + "# resources. For example, if your application contains many locales and\n" + + "# you wish to release multiple smaller apks instead of a large one, you can\n" + + "# define configuration to create apks with limited language sets.\n" + + "# Format is a comma separated list of configuration names. For each\n" + + "# configuration, a property will declare the resource configurations to\n" + + "# include. Example:\n" + + "# " + PROPERTY_APK_CONFIGS +"=european,northamerica\n" + + "# " + ApkConfigurationHelper.CONFIG_PREFIX + "european=en,fr,it,de,es\n" + + "# " + ApkConfigurationHelper.CONFIG_PREFIX + "northamerica=en,es\n"); + COMMENT_MAP.put(PROPERTY_SDK, + "# location of the SDK. This is only used by Ant\n" + + "# For customization when using a Version Control System, please read the\n" + + "# header note.\n"); + } + + private final String mProjectFolderOsPath; + private final Map<String, String> mProperties; + private final PropertyType mType; + + /** + * Loads a project properties file and return a {@link ProjectProperties} object + * containing the properties + * + * @param projectFolderOsPath the project folder. + * @param type One the possible {@link PropertyType}s. + */ + public static ProjectProperties load(String projectFolderOsPath, PropertyType type) { + File projectFolder = new File(projectFolderOsPath); + if (projectFolder.isDirectory()) { + File defaultFile = new File(projectFolder, type.mFilename); + if (defaultFile.isFile()) { + Map<String, String> map = SdkManager.parsePropertyFile(defaultFile, null /* log */); + if (map != null) { + return new ProjectProperties(projectFolderOsPath, map, type); + } + } + } + return null; + } + + /** + * Merges all properties from the given file into the current properties. + * <p/> + * This emulates the Ant behavior: existing properties are <em>not</em> overriden. + * Only new undefined properties become defined. + * <p/> + * Typical usage: + * <ul> + * <li>Create a ProjectProperties with {@link PropertyType#BUILD} + * <li>Merge in values using {@link PropertyType#DEFAULT} + * <li>The result is that this contains all the properties from default plus those + * overridden by the build.properties file. + * </ul> + * + * @param type One the possible {@link PropertyType}s. + * @return this object, for chaining. + */ + public ProjectProperties merge(PropertyType type) { + File projectFolder = new File(mProjectFolderOsPath); + if (projectFolder.isDirectory()) { + File defaultFile = new File(projectFolder, type.mFilename); + if (defaultFile.isFile()) { + Map<String, String> map = SdkManager.parsePropertyFile(defaultFile, null /* log */); + if (map != null) { + for(Entry<String, String> entry : map.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (!mProperties.containsKey(key) && value != null) { + mProperties.put(key, value); + } + } + } + } + } + return this; + } + + /** + * Creates a new project properties object, with no properties. + * <p/>The file is not created until {@link #save()} is called. + * @param projectFolderOsPath the project folder. + * @param type + */ + public static ProjectProperties create(String projectFolderOsPath, PropertyType type) { + // create and return a ProjectProperties with an empty map. + return new ProjectProperties(projectFolderOsPath, new HashMap<String, String>(), type); + } + + /** + * Sets a new properties. If a property with the same name already exists, it is replaced. + * @param name the name of the property. + * @param value the value of the property. + */ + public void setProperty(String name, String value) { + mProperties.put(name, value); + } + + /** + * Sets the target property to the given {@link IAndroidTarget} object. + * @param target the Android target. + */ + public void setAndroidTarget(IAndroidTarget target) { + assert mType == PropertyType.DEFAULT; + mProperties.put(PROPERTY_TARGET, target.hashString()); + } + + /** + * Returns the value of a property. + * @param name the name of the property. + * @return the property value or null if the property is not set. + */ + public String getProperty(String name) { + return mProperties.get(name); + } + + /** + * Removes a property and returns its previous value (or null if the property did not exist). + * @param name the name of the property to remove. + */ + public String removeProperty(String name) { + return mProperties.remove(name); + } + + /** + * Saves the property file. + * @throws IOException + */ + public void save() throws IOException { + File toSave = new File(mProjectFolderOsPath, mType.mFilename); + + FileWriter writer = new FileWriter(toSave); + + // write the header + writer.write(mType.mHeader); + + // write the properties. + for (Entry<String, String> entry : mProperties.entrySet()) { + String comment = COMMENT_MAP.get(entry.getKey()); + if (comment != null) { + writer.write(comment); + } + writer.write(String.format("%s=%s\n", entry.getKey(), entry.getValue())); + } + + // close the file to flush + writer.close(); + } + + /** + * Private constructor. + * <p/> + * Use {@link #load(String, PropertyType)} or {@link #create(String, PropertyType)} + * to instantiate. + */ + private ProjectProperties(String projectFolderOsPath, Map<String, String> map, + PropertyType type) { + mProjectFolderOsPath = projectFolderOsPath; + mProperties = map; + mType = type; + } +} diff --git a/sdkmanager/libs/sdkuilib/.classpath b/sdkmanager/libs/sdkuilib/.classpath new file mode 100644 index 0000000..eb5af7e --- /dev/null +++ b/sdkmanager/libs/sdkuilib/.classpath @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/> + <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/sdkmanager/libs/sdkuilib/.project b/sdkmanager/libs/sdkuilib/.project new file mode 100644 index 0000000..da430c8 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>SdkUiLib</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/sdkmanager/libs/sdkuilib/Android.mk b/sdkmanager/libs/sdkuilib/Android.mk new file mode 100644 index 0000000..8e0bc23 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/Android.mk @@ -0,0 +1,4 @@ +# Copyright 2008 The Android Open Source Project +# +SDKUILIB_LOCAL_DIR := $(call my-dir) +include $(SDKUILIB_LOCAL_DIR)/src/Android.mk diff --git a/sdkmanager/libs/sdkuilib/README b/sdkmanager/libs/sdkuilib/README new file mode 100644 index 0000000..d66b84a --- /dev/null +++ b/sdkmanager/libs/sdkuilib/README @@ -0,0 +1,11 @@ +Using the Eclipse projects for ddmuilib. + +ddmuilib requires SWT to compile. + +SWT is available in the depot under prebuild/<platform>/swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar +available at prebuild/<platform>/swt.
\ No newline at end of file diff --git a/sdkmanager/libs/sdkuilib/src/Android.mk b/sdkmanager/libs/sdkuilib/src/Android.mk new file mode 100644 index 0000000..2d3c774 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/src/Android.mk @@ -0,0 +1,21 @@ +# Copyright 2008 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +# no resources yet. +# LOCAL_JAVA_RESOURCE_DIRS := resources + +LOCAL_JAVA_LIBRARIES := \ + sdklib \ + swt \ + org.eclipse.jface_3.2.0.I20060605-1400 \ + org.eclipse.equinox.common_3.2.0.v20060603 \ + org.eclipse.core.commands_3.2.0.I20060605-1400 + +LOCAL_MODULE := sdkuilib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigEditDialog.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigEditDialog.java new file mode 100644 index 0000000..1460fd7 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigEditDialog.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2009 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.sdkuilib; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Edit dialog to create/edit APK configuration. The dialog displays 2 text fields for the config + * name and its filter. + */ +class ApkConfigEditDialog extends Dialog implements ModifyListener, VerifyListener { + + private String mName; + private String mFilter; + private Text mNameField; + private Text mFilterField; + private Button mOkButton; + + /** + * Creates an edit dialog with optional initial values for the name and filter. + * @param name optional value for the name. Can be null. + * @param filter optional value for the filter. Can be null. + * @param parentShell the parent shell. + */ + protected ApkConfigEditDialog(String name, String filter, Shell parentShell) { + super(parentShell); + mName = name; + mFilter = filter; + } + + /** + * Returns the name of the config. This is only valid if the user clicked OK and {@link #open()} + * returned {@link Window#OK} + */ + public String getName() { + return mName; + } + + /** + * Returns the filter for the config. This is only valid if the user clicked OK and + * {@link #open()} returned {@link Window#OK} + */ + public String getFilter() { + return mFilter; + } + + @Override + protected Control createContents(Composite parent) { + Control control = super.createContents(parent); + + mOkButton = getButton(IDialogConstants.OK_ID); + validateButtons(); + + return control; + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + GridLayout layout; + composite.setLayout(layout = new GridLayout(2, false)); + layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); + layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN); + layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); + layout.horizontalSpacing = convertHorizontalDLUsToPixels( + IDialogConstants.HORIZONTAL_SPACING); + + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Label l = new Label(composite, SWT.NONE); + l.setText("Name"); + + mNameField = new Text(composite, SWT.BORDER); + mNameField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNameField.addVerifyListener(this); + if (mName != null) { + mNameField.setText(mName); + } + mNameField.addModifyListener(this); + + l = new Label(composite, SWT.NONE); + l.setText("Filter"); + + mFilterField = new Text(composite, SWT.BORDER); + mFilterField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (mFilter != null) { + mFilterField.setText(mFilter); + } + mFilterField.addVerifyListener(this); + mFilterField.addModifyListener(this); + + applyDialogFont(composite); + return composite; + } + + /** + * Validates the OK button based on the content of the 2 text fields. + */ + private void validateButtons() { + mOkButton.setEnabled(mNameField.getText().trim().length() > 0 && + mFilterField.getText().trim().length() > 0); + } + + @Override + protected void okPressed() { + mName = mNameField.getText(); + mFilter = mFilterField.getText().trim(); + super.okPressed(); + } + + /** + * Callback for text modification in the 2 text fields. + */ + public void modifyText(ModifyEvent e) { + validateButtons(); + } + + /** + * Callback to ensure the content of the text field are proper. + */ + public void verifyText(VerifyEvent e) { + Text source = ((Text)e.getSource()); + if (source == mNameField) { + // check for a-zA-Z0-9. + final String text = e.text; + final int len = text.length(); + for (int i = 0 ; i < len; i++) { + char letter = text.charAt(i); + if (letter > 255 || Character.isLetterOrDigit(letter) == false) { + e.doit = false; + return; + } + } + } else if (source == mFilterField) { + // we can't validate the content as its typed, but we can at least ensure the characters + // are valid. Same as mNameFiled + the comma. + final String text = e.text; + final int len = text.length(); + for (int i = 0 ; i < len; i++) { + char letter = text.charAt(i); + if (letter > 255 || (Character.isLetterOrDigit(letter) == false && letter != ',')) { + e.doit = false; + return; + } + } + } + } +} diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigWidget.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigWidget.java new file mode 100644 index 0000000..6bf1df3 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigWidget.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2009 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.sdkuilib; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * The APK Configuration widget is a table that is added to the given parent composite. + * <p/> + * To use, create it using {@link #ApkConfigWidget(Composite)} then + * call {@link #fillTable(Map) to set the initial list of configurations. + */ +public class ApkConfigWidget { + private final static int INDEX_NAME = 0; + private final static int INDEX_FILTER = 1; + + private Table mApkConfigTable; + private Button mEditButton; + private Button mDelButton; + + public ApkConfigWidget(final Composite parent) { + final Composite apkConfigComp = new Composite(parent, SWT.NONE); + apkConfigComp.setLayoutData(new GridData(GridData.FILL_BOTH)); + apkConfigComp.setLayout(new GridLayout(2, false)); + + mApkConfigTable = new Table(apkConfigComp, SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); + mApkConfigTable.setHeaderVisible(true); + mApkConfigTable.setLinesVisible(true); + + GridData data = new GridData(); + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mApkConfigTable.setLayoutData(data); + + // create the table columns + final TableColumn column0 = new TableColumn(mApkConfigTable, SWT.NONE); + column0.setText("Name"); + column0.setWidth(100); + final TableColumn column1 = new TableColumn(mApkConfigTable, SWT.NONE); + column1.setText("Configuration"); + column1.setWidth(100); + + Composite buttonComp = new Composite(apkConfigComp, SWT.NONE); + buttonComp.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + GridLayout gl; + buttonComp.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + + Button newButton = new Button(buttonComp, SWT.PUSH | SWT.FLAT); + newButton.setText("New..."); + newButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mEditButton = new Button(buttonComp, SWT.PUSH | SWT.FLAT); + mEditButton.setText("Edit..."); + mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mDelButton = new Button(buttonComp, SWT.PUSH | SWT.FLAT); + mDelButton.setText("Delete"); + mDelButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + newButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + ApkConfigEditDialog dlg = new ApkConfigEditDialog(null /*name*/, null /*filter*/, + apkConfigComp.getShell()); + if (dlg.open() == Dialog.OK) { + TableItem item = new TableItem(mApkConfigTable, SWT.NONE); + item.setText(INDEX_NAME, dlg.getName()); + item.setText(INDEX_FILTER, dlg.getFilter()); + + onSelectionChanged(); + } + } + }); + + mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the current selection (single mode so we don't care about any item beyond + // index 0). + TableItem[] items = mApkConfigTable.getSelection(); + if (items.length != 0) { + ApkConfigEditDialog dlg = new ApkConfigEditDialog( + items[0].getText(INDEX_NAME), items[0].getText(INDEX_FILTER), + apkConfigComp.getShell()); + if (dlg.open() == Dialog.OK) { + items[0].setText(INDEX_NAME, dlg.getName()); + items[0].setText(INDEX_FILTER, dlg.getFilter()); + } + } + } + }); + + mDelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the current selection (single mode so we don't care about any item beyond + // index 0). + int[] indices = mApkConfigTable.getSelectionIndices(); + if (indices.length != 0) { + TableItem item = mApkConfigTable.getItem(indices[0]); + if (MessageDialog.openQuestion(parent.getShell(), + "Apk Configuration deletion", + String.format( + "Are you sure you want to delete configuration '%1$s'?", + item.getText(INDEX_NAME)))) { + // delete the item. + mApkConfigTable.remove(indices[0]); + + onSelectionChanged(); + } + } + } + }); + + // Add a listener to resize the column to the full width of the table + mApkConfigTable.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = mApkConfigTable.getClientArea(); + column0.setWidth(r.width * 30 / 100); // 30% + column1.setWidth(r.width * 70 / 100); // 70% + } + }); + + // add a selection listener on the table, to enable/disable buttons. + mApkConfigTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onSelectionChanged(); + } + }); + } + + public void fillTable(Map<String, String> apkConfigMap) { + // get the names in a list so that we can sort them. + if (apkConfigMap != null) { + Set<String> keys = apkConfigMap.keySet(); + String[] keyArray = keys.toArray(new String[keys.size()]); + Arrays.sort(keyArray); + + for (String key : keyArray) { + TableItem item = new TableItem(mApkConfigTable, SWT.NONE); + item.setText(INDEX_NAME, key); + item.setText(INDEX_FILTER, apkConfigMap.get(key)); + } + } + + onSelectionChanged(); + } + + public Map<String, String> getApkConfigs() { + // go through all the items from the table and fill a new map + HashMap<String, String> map = new HashMap<String, String>(); + + TableItem[] items = mApkConfigTable.getItems(); + for (TableItem item : items) { + map.put(item.getText(INDEX_NAME), item.getText(INDEX_FILTER)); + } + + return map; + } + + /** + * Handles table selection changes. + */ + private void onSelectionChanged() { + if (mApkConfigTable.getSelectionCount() > 0) { + mEditButton.setEnabled(true); + mDelButton.setEnabled(true); + } else { + mEditButton.setEnabled(false); + mDelButton.setEnabled(false); + } + } +} diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/AvdSelector.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/AvdSelector.java new file mode 100644 index 0000000..9d0b928 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/AvdSelector.java @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.sdkuilib; + +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.avd.AvdManager.AvdInfo; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; + + +/** + * The AVD selector is a table that is added to the given parent composite. + * <p/> + * To use, create it using {@link #AvdSelector(Composite, AvdInfo[], boolean)} then + * call {@link #setSelection(AvdInfo)}, {@link #setSelectionListener(SelectionListener)} + * and finally use {@link #getFirstSelected()} or {@link #getAllSelected()} to retrieve the + * selection. + */ +public final class AvdSelector { + + private AvdInfo[] mAvds; + private final boolean mAllowMultipleSelection; + private SelectionListener mSelectionListener; + private Table mTable; + private Label mDescription; + + /** + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered + * by a {@link IAndroidTarget}. + * <p/>Only the {@link AvdInfo} able to run application developed for the given + * {@link IAndroidTarget} will be displayed. + * + * @param parent The parent composite where the selector will be added. + * @param avds The list of AVDs. This is <em>not</em> copied, the caller must not modify. + * @param allowMultipleSelection True if more than one SDK target can be selected at the same + * time. + */ + public AvdSelector(Composite parent, AvdInfo[] avds, IAndroidTarget filter, + boolean allowMultipleSelection) { + mAvds = avds; + + // Layout has 1 column + Composite group = new Composite(parent, SWT.NONE); + group.setLayout(new GridLayout()); + group.setLayoutData(new GridData(GridData.FILL_BOTH)); + group.setFont(parent.getFont()); + + mAllowMultipleSelection = allowMultipleSelection; + mTable = new Table(group, SWT.CHECK | SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); + mTable.setHeaderVisible(true); + mTable.setLinesVisible(false); + + GridData data = new GridData(); + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + + mDescription = new Label(group, SWT.WRAP); + mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // create the table columns + final TableColumn column0 = new TableColumn(mTable, SWT.NONE); + column0.setText("AVD Name"); + final TableColumn column1 = new TableColumn(mTable, SWT.NONE); + column1.setText("Target Name"); + final TableColumn column2 = new TableColumn(mTable, SWT.NONE); + column2.setText("API Level"); + final TableColumn column3 = new TableColumn(mTable, SWT.NONE); + column3.setText("SDK"); + + adjustColumnsWidth(mTable, column0, column1, column2, column3); + setupSelectionListener(mTable); + fillTable(mTable, filter); + setupTooltip(mTable); + } + + /** + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}. + * + * @param parent The parent composite where the selector will be added. + * @param avds The list of AVDs. This is <em>not</em> copied, the caller must not modify. + * @param allowMultipleSelection True if more than one SDK target can be selected at the same + * time. + */ + public AvdSelector(Composite parent, AvdInfo[] avds, boolean allowMultipleSelection) { + this(parent, avds, null /* filter */, allowMultipleSelection); + } + + + public void setTableHeightHint(int heightHint) { + GridData data = new GridData(); + data.heightHint = heightHint; + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + } + + /** + * Sets a new set of AVD, with an optional filter. + * <p/>This must be called from the UI thread. + * + * @param avds The list of AVDs. This is <em>not</em> copied, the caller must not modify. + * @param filter An IAndroidTarget. If non-null, only AVD whose target are compatible with the + * filter target will displayed an available for selection. + */ + public void setAvds(AvdInfo[] avds, IAndroidTarget filter) { + mAvds = avds; + fillTable(mTable, filter); + } + + /** + * Returns the list of known AVDs. + * <p/> + * This is not a copy. Callers must <em>not</em> modify this array. + */ + public AvdInfo[] getAvds() { + return mAvds; + } + + /** + * Sets a selection listener. Set it to null to remove it. + * The listener will be called <em>after</em> this table processed its selection + * events so that the caller can see the updated state. + * <p/> + * The event's item contains a {@link TableItem}. + * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. + * <p/> + * It is recommended that the caller uses the {@link #getFirstSelected()} and + * {@link #getAllSelected()} methods instead. + * + * @param selectionListener The new listener or null to remove it. + */ + public void setSelectionListener(SelectionListener selectionListener) { + mSelectionListener = selectionListener; + } + + /** + * Sets the current target selection. + * <p/> + * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selection + * @return true if the target could be selected, false otherwise. + */ + public boolean setSelection(AvdInfo target) { + boolean found = false; + boolean modified = false; + for (TableItem i : mTable.getItems()) { + if ((AvdInfo) i.getData() == target) { + found = true; + if (!i.getChecked()) { + modified = true; + i.setChecked(true); + } + } else if (i.getChecked()) { + modified = true; + i.setChecked(false); + } + } + + if (modified && mSelectionListener != null) { + mSelectionListener.widgetSelected(null); + } + + return found; + } + + /** + * Returns all selected items. + * This is useful when the table is in multiple-selection mode. + * + * @see #getFirstSelected() + * @return An array of selected items. The list can be empty but not null. + */ + public AvdInfo[] getAllSelected() { + ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>(); + for (TableItem i : mTable.getItems()) { + if (i.getChecked()) { + list.add((IAndroidTarget) i.getData()); + } + } + return list.toArray(new AvdInfo[list.size()]); + } + + /** + * Returns the first selected item. + * This is useful when the table is in single-selection mode. + * + * @see #getAllSelected() + * @return The first selected item or null. + */ + public AvdInfo getFirstSelected() { + for (TableItem i : mTable.getItems()) { + if (i.getChecked()) { + return (AvdInfo) i.getData(); + } + } + return null; + } + + /** + * Enables the receiver if the argument is true, and disables it otherwise. + * A disabled control is typically not selectable from the user interface + * and draws with an inactive or "grayed" look. + * + * @param enabled the new enabled state. + */ + public void setEnabled(boolean enabled) { + mTable.setEnabled(enabled); + mDescription.setEnabled(enabled); + } + + /** + * Adds a listener to adjust the columns width when the parent is resized. + * <p/> + * If we need something more fancy, we might want to use this: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co + */ + private void adjustColumnsWidth(final Table table, + final TableColumn column0, + final TableColumn column1, + final TableColumn column2, + final TableColumn column3) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + column0.setWidth(r.width * 30 / 100); // 30% + column1.setWidth(r.width * 45 / 100); // 45% + column2.setWidth(r.width * 15 / 100); // 15% + column3.setWidth(r.width * 10 / 100); // 10% + } + }); + } + + + /** + * Creates a selection listener that will check or uncheck the whole line when + * double-clicked (aka "the default selection"). + */ + private void setupSelectionListener(final Table table) { + // Add a selection listener that will check/uncheck items when they are double-clicked + table.addSelectionListener(new SelectionListener() { + /** Default selection means double-click on "most" platforms */ + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + i.setChecked(!i.getChecked()); + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetDefaultSelected(e); + } + } + + public void widgetSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetSelected(e); + } + } + + /** + * If we're not in multiple selection mode, uncheck all other + * items when this one is selected. + */ + private void enforceSingleSelection(TableItem item) { + if (!mAllowMultipleSelection && item.getChecked()) { + Table parentTable = item.getParent(); + for (TableItem i2 : parentTable.getItems()) { + if (i2 != item && i2.getChecked()) { + i2.setChecked(false); + } + } + } + } + }); + } + + /** + * Fills the table with all AVD. + * The table columns are: + * <ul> + * <li>column 0: sdk name + * <li>column 1: sdk vendor + * <li>column 2: sdk api name + * <li>column 3: sdk version + * </ul> + */ + private void fillTable(final Table table, IAndroidTarget filter) { + table.removeAll(); + if (mAvds != null && mAvds.length > 0) { + table.setEnabled(true); + for (AvdInfo avd : mAvds) { + if (filter == null || filter.isCompatibleBaseFor(avd.getTarget())) { + TableItem item = new TableItem(table, SWT.NONE); + item.setData(avd); + item.setText(0, avd.getName()); + IAndroidTarget target = avd.getTarget(); + item.setText(1, target.getFullName()); + item.setText(2, target.getApiVersionName()); + item.setText(3, Integer.toString(target.getApiVersionNumber())); + } + } + } + + if (table.getItemCount() == 0) { + table.setEnabled(false); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, "--"); + item.setText(1, "No AVD available"); + item.setText(2, "--"); + item.setText(3, "--"); + } + } + + /** + * Sets up a tooltip that displays the current item description. + * <p/> + * Displaying a tooltip over the table looks kind of odd here. Instead we actually + * display the description in a label under the table. + */ + private void setupTooltip(final Table table) { + /* + * Reference: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup + */ + + final Listener listener = new Listener() { + public void handleEvent(Event event) { + + switch(event.type) { + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + return; + + case SWT.MouseHover: + updateDescription(table.getItem(new Point(event.x, event.y))); + break; + + case SWT.Selection: + if (event.item instanceof TableItem) { + updateDescription((TableItem) event.item); + } + break; + + default: + return; + } + + } + }; + + table.addListener(SWT.Dispose, listener); + table.addListener(SWT.KeyDown, listener); + table.addListener(SWT.MouseMove, listener); + table.addListener(SWT.MouseHover, listener); + } + + /** + * Updates the description label with the path of the item's AVD, if any. + */ + private void updateDescription(TableItem item) { + if (item != null) { + Object data = item.getData(); + if (data instanceof AvdInfo) { + String newTooltip = ((AvdInfo) data).getPath(); + mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ + } + } + } +} diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/SdkTargetSelector.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/SdkTargetSelector.java new file mode 100644 index 0000000..5f9e9c2 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/SdkTargetSelector.java @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.sdkuilib; + +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; + + +/** + * The SDK target selector is a table that is added to the given parent composite. + * <p/> + * To use, create it using {@link #SdkTargetSelector(Composite, IAndroidTarget[], boolean)} then + * call {@link #setSelection(IAndroidTarget)}, {@link #setSelectionListener(SelectionListener)} + * and finally use {@link #getFirstSelected()} or {@link #getAllSelected()} to retrieve the + * selection. + */ +public class SdkTargetSelector { + + private IAndroidTarget[] mTargets; + private final boolean mAllowSelection; + private final boolean mAllowMultipleSelection; + private SelectionListener mSelectionListener; + private Table mTable; + private Label mDescription; + private Composite mInnerGroup; + + /** + * Creates a new SDK Target Selector. + * + * @param parent The parent composite where the selector will be added. + * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify. + * Targets can be null or an empty array, in which case the table is disabled. + * @param allowMultipleSelection True if more than one SDK target can be selected at the same + * time. + */ + public SdkTargetSelector(Composite parent, IAndroidTarget[] targets, + boolean allowMultipleSelection) { + this(parent, targets, true /*allowSelection*/, allowMultipleSelection); + } + + /** + * Creates a new SDK Target Selector. + * + * @param parent The parent composite where the selector will be added. + * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify. + * Targets can be null or an empty array, in which case the table is disabled. + * @param allowSelection True if selection is enabled. + * @param allowMultipleSelection True if more than one SDK target can be selected at the same + * time. Used only if allowSelection is true. + */ + public SdkTargetSelector(Composite parent, IAndroidTarget[] targets, + boolean allowSelection, + boolean allowMultipleSelection) { + // Layout has 1 column + mInnerGroup = new Composite(parent, SWT.NONE); + mInnerGroup.setLayout(new GridLayout()); + mInnerGroup.setLayoutData(new GridData(GridData.FILL_BOTH)); + mInnerGroup.setFont(parent.getFont()); + + mAllowSelection = allowSelection; + mAllowMultipleSelection = allowMultipleSelection; + int style = SWT.BORDER; + if (allowSelection) { + style |= SWT.CHECK | SWT.FULL_SELECTION; + } + if (!mAllowMultipleSelection) { + style |= SWT.SINGLE; + } + mTable = new Table(mInnerGroup, style); + mTable.setHeaderVisible(true); + mTable.setLinesVisible(false); + + GridData data = new GridData(); + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + + mDescription = new Label(mInnerGroup, SWT.WRAP); + mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // create the table columns + final TableColumn column0 = new TableColumn(mTable, SWT.NONE); + column0.setText("SDK Target"); + final TableColumn column1 = new TableColumn(mTable, SWT.NONE); + column1.setText("Vendor"); + final TableColumn column2 = new TableColumn(mTable, SWT.NONE); + column2.setText("Version"); + final TableColumn column3 = new TableColumn(mTable, SWT.NONE); + column3.setText("API Level"); + + adjustColumnsWidth(mTable, column0, column1, column2, column3); + setupSelectionListener(mTable); + setTargets(targets); + setupTooltip(mTable); + } + + /** + * Returns the layout data of the inner composite widget that contains the target selector. + * By default the layout data is set to a {@link GridData} with a {@link GridData#FILL_BOTH} + * mode. + * <p/> + * This can be useful if you want to change the {@link GridData#horizontalSpan} for example. + */ + public Object getLayoutData() { + return mInnerGroup.getLayoutData(); + } + + /** + * Returns the list of known targets. + * <p/> + * This is not a copy. Callers must <em>not</em> modify this array. + */ + public IAndroidTarget[] getTargets() { + return mTargets; + } + + /** + * Changes the targets of the SDK Target Selector. + * + * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify. + */ + public void setTargets(IAndroidTarget[] targets) { + mTargets = targets; + fillTable(mTable); + } + + /** + * Sets a selection listener. Set it to null to remove it. + * The listener will be called <em>after</em> this table processed its selection + * events so that the caller can see the updated state. + * <p/> + * The event's item contains a {@link TableItem}. + * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. + * <p/> + * It is recommended that the caller uses the {@link #getFirstSelected()} and + * {@link #getAllSelected()} methods instead. + * + * @param selectionListener The new listener or null to remove it. + */ + public void setSelectionListener(SelectionListener selectionListener) { + mSelectionListener = selectionListener; + } + + /** + * Sets the current target selection. + * <p/> + * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selection + * @return true if the target could be selected, false otherwise. + */ + public boolean setSelection(IAndroidTarget target) { + if (!mAllowSelection) { + return false; + } + + boolean found = false; + boolean modified = false; + for (TableItem i : mTable.getItems()) { + if ((IAndroidTarget) i.getData() == target) { + found = true; + if (!i.getChecked()) { + modified = true; + i.setChecked(true); + } + } else if (i.getChecked()) { + modified = true; + i.setChecked(false); + } + } + + if (modified && mSelectionListener != null) { + mSelectionListener.widgetSelected(null); + } + + return found; + } + + /** + * Returns all selected items. + * This is useful when the table is in multiple-selection mode. + * + * @see #getFirstSelected() + * @return An array of selected items. The list can be empty but not null. + */ + public IAndroidTarget[] getAllSelected() { + ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>(); + for (TableItem i : mTable.getItems()) { + if (i.getChecked()) { + list.add((IAndroidTarget) i.getData()); + } + } + return list.toArray(new IAndroidTarget[list.size()]); + } + + /** + * Returns the first selected item. + * This is useful when the table is in single-selection mode. + * + * @see #getAllSelected() + * @return The first selected item or null. + */ + public IAndroidTarget getFirstSelected() { + for (TableItem i : mTable.getItems()) { + if (i.getChecked()) { + return (IAndroidTarget) i.getData(); + } + } + return null; + } + + /** + * Adds a listener to adjust the columns width when the parent is resized. + * <p/> + * If we need something more fancy, we might want to use this: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co + */ + private void adjustColumnsWidth(final Table table, + final TableColumn column0, + final TableColumn column1, + final TableColumn column2, + final TableColumn column3) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + column0.setWidth(r.width * 30 / 100); // 30% + column1.setWidth(r.width * 45 / 100); // 45% + column2.setWidth(r.width * 15 / 100); // 15% + column3.setWidth(r.width * 10 / 100); // 10% + } + }); + } + + + /** + * Creates a selection listener that will check or uncheck the whole line when + * double-clicked (aka "the default selection"). + */ + private void setupSelectionListener(final Table table) { + if (!mAllowSelection) { + return; + } + + // Add a selection listener that will check/uncheck items when they are double-clicked + table.addSelectionListener(new SelectionListener() { + /** Default selection means double-click on "most" platforms */ + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + i.setChecked(!i.getChecked()); + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetDefaultSelected(e); + } + } + + public void widgetSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetSelected(e); + } + } + + /** + * If we're not in multiple selection mode, uncheck all other + * items when this one is selected. + */ + private void enforceSingleSelection(TableItem item) { + if (!mAllowMultipleSelection && item.getChecked()) { + Table parentTable = item.getParent(); + for (TableItem i2 : parentTable.getItems()) { + if (i2 != item && i2.getChecked()) { + i2.setChecked(false); + } + } + } + } + }); + } + + + /** + * Fills the table with all SDK targets. + * The table columns are: + * <ul> + * <li>column 0: sdk name + * <li>column 1: sdk vendor + * <li>column 2: sdk api name + * <li>column 3: sdk version + * </ul> + */ + private void fillTable(final Table table) { + + table.removeAll(); + + if (mTargets != null && mTargets.length > 0) { + table.setEnabled(true); + for (IAndroidTarget target : mTargets) { + TableItem item = new TableItem(table, SWT.NONE); + item.setData(target); + item.setText(0, target.getName()); + item.setText(1, target.getVendor()); + item.setText(2, target.getApiVersionName()); + item.setText(3, Integer.toString(target.getApiVersionNumber())); + } + } else { + table.setEnabled(false); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, "--"); + item.setText(1, "No target available"); + item.setText(2, "--"); + item.setText(3, "--"); + } + } + + /** + * Sets up a tooltip that displays the current item description. + * <p/> + * Displaying a tooltip over the table looks kind of odd here. Instead we actually + * display the description in a label under the table. + */ + private void setupTooltip(final Table table) { + /* + * Reference: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup + */ + + final Listener listener = new Listener() { + public void handleEvent(Event event) { + + switch(event.type) { + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + return; + + case SWT.MouseHover: + updateDescription(table.getItem(new Point(event.x, event.y))); + break; + + case SWT.Selection: + if (event.item instanceof TableItem) { + updateDescription((TableItem) event.item); + } + break; + + default: + return; + } + + } + }; + + table.addListener(SWT.Dispose, listener); + table.addListener(SWT.KeyDown, listener); + table.addListener(SWT.MouseMove, listener); + table.addListener(SWT.MouseHover, listener); + } + + /** + * Updates the description label with the description of the item's android target, if any. + */ + private void updateDescription(TableItem item) { + if (item != null) { + Object data = item.getData(); + if (data instanceof IAndroidTarget) { + String newTooltip = ((IAndroidTarget) data).getDescription(); + mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ + } + } + } +} |