/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ant;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
import com.android.sdklib.ISdkLog;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.SdkManager;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.xml.AndroidManifest;
import com.android.sdklib.xml.AndroidXPathFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Path.PathElement;
import org.xml.sax.InputSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashSet;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
/**
* Task to resolve the target of the current Android project.
*
* Out params:
* bootClassPathOut
: The boot class path of the project.
*
* androidJarFileOut
: the android.jar used by the project.
*
* androidAidlFileOut
: the framework.aidl used by the project.
*
* targetApiOut
: the build API level.
*
* minSdkVersionOut
: the app's minSdkVersion.
*
*/
public class GetTargetTask extends Task {
private String mBootClassPathOut;
private String mAndroidJarFileOut;
private String mAndroidAidlFileOut;
private String mTargetApiOut;
private String mMinSdkVersionOut;
public void setBootClassPathOut(String bootClassPathOut) {
mBootClassPathOut = bootClassPathOut;
}
public void setAndroidJarFileOut(String androidJarFileOut) {
mAndroidJarFileOut = androidJarFileOut;
}
public void setAndroidAidlFileOut(String androidAidlFileOut) {
mAndroidAidlFileOut = androidAidlFileOut;
}
public void setTargetApiOut(String targetApiOut) {
mTargetApiOut = targetApiOut;
}
public void setMinSdkVersionOut(String minSdkVersionOut) {
mMinSdkVersionOut = minSdkVersionOut;
}
@Override
public void execute() throws BuildException {
if (mBootClassPathOut == null) {
throw new BuildException("Missing attribute bootClassPathOut");
}
if (mAndroidJarFileOut == null) {
throw new BuildException("Missing attribute androidJarFileOut");
}
if (mAndroidAidlFileOut == null) {
throw new BuildException("Missing attribute androidAidlFileOut");
}
if (mTargetApiOut == null) {
throw new BuildException("Missing attribute targetApiOut");
}
if (mMinSdkVersionOut == null) {
throw new BuildException("Missing attribute mMinSdkVersionOut");
}
Project antProject = getProject();
// get the SDK location
File sdkDir = TaskHelper.getSdkLocation(antProject);
// get the target property value
String targetHashString = antProject.getProperty(ProjectProperties.PROPERTY_TARGET);
if (targetHashString == null) {
throw new BuildException("Android Target is not set.");
}
// load up the sdk targets.
final ArrayList messages = new ArrayList();
SdkManager manager = SdkManager.createManager(sdkDir.getPath(), new ISdkLog() {
@Override
public void error(Throwable t, String errorFormat, Object... args) {
if (errorFormat != null) {
messages.add(String.format("Error: " + errorFormat, args));
}
if (t != null) {
messages.add("Error: " + t.getMessage());
}
}
@Override
public void printf(String msgFormat, Object... args) {
messages.add(String.format(msgFormat, args));
}
@Override
public void warning(String warningFormat, Object... args) {
messages.add(String.format("Warning: " + warningFormat, args));
}
});
if (manager == null) {
// since we failed to parse the SDK, lets display the parsing output.
for (String msg : messages) {
System.out.println(msg);
}
throw new BuildException("Failed to parse SDK content.");
}
// resolve it
IAndroidTarget androidTarget = manager.getTargetFromHashString(targetHashString);
if (androidTarget == null) {
throw new BuildException(String.format(
"Unable to resolve project target '%s'", targetHashString));
}
// display the project info
System.out.println( "Project Target: " + androidTarget.getName());
if (androidTarget.isPlatform() == false) {
System.out.println("Vendor: " + androidTarget.getVendor());
System.out.println("Platform Version: " + androidTarget.getVersionName());
}
System.out.println( "API level: " + androidTarget.getVersion().getApiString());
antProject.setProperty(mMinSdkVersionOut,
Integer.toString(androidTarget.getVersion().getApiLevel()));
// always check the manifest minSdkVersion.
checkManifest(antProject, androidTarget.getVersion());
// sets up the properties to find android.jar/framework.aidl/target tools
String androidJar = androidTarget.getPath(IAndroidTarget.ANDROID_JAR);
antProject.setProperty(mAndroidJarFileOut, androidJar);
String androidAidl = androidTarget.getPath(IAndroidTarget.ANDROID_AIDL);
antProject.setProperty(mAndroidAidlFileOut, androidAidl);
// sets up the boot classpath
// create the Path object
Path bootclasspath = new Path(antProject);
// create a PathElement for the framework jar
PathElement element = bootclasspath.createPathElement();
element.setPath(androidJar);
// create PathElement for each optional library.
IOptionalLibrary[] libraries = androidTarget.getOptionalLibraries();
if (libraries != null) {
HashSet visitedJars = new HashSet();
for (IOptionalLibrary library : libraries) {
String jarPath = library.getJarPath();
if (visitedJars.contains(jarPath) == false) {
visitedJars.add(jarPath);
element = bootclasspath.createPathElement();
element.setPath(library.getJarPath());
}
}
}
// sets the path in the project with a reference
antProject.addReference(mBootClassPathOut, bootclasspath);
}
/**
* Checks the manifest minSdkVersion
attribute.
* @param antProject the ant project
* @param androidVersion the version of the platform the project is compiling against.
*/
private void checkManifest(Project antProject, AndroidVersion androidVersion) {
try {
File manifest = new File(antProject.getBaseDir(), SdkConstants.FN_ANDROID_MANIFEST_XML);
XPath xPath = AndroidXPathFactory.newXPath();
// check the package name.
String value = xPath.evaluate(
"/" + AndroidManifest.NODE_MANIFEST +
"/@" + AndroidManifest.ATTRIBUTE_PACKAGE,
new InputSource(new FileInputStream(manifest)));
if (value != null) { // aapt will complain if it's missing.
// only need to check that the package has 2 segments
if (value.indexOf('.') == -1) {
throw new BuildException(String.format(
"Application package '%1$s' must have a minimum of 2 segments.",
value));
}
}
// check the minSdkVersion value
value = xPath.evaluate(
"/" + AndroidManifest.NODE_MANIFEST +
"/" + AndroidManifest.NODE_USES_SDK +
"/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX + ":" +
AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
new InputSource(new FileInputStream(manifest)));
if (androidVersion.isPreview()) {
// in preview mode, the content of the minSdkVersion must match exactly the
// platform codename.
String codeName = androidVersion.getCodename();
if (codeName.equals(value) == false) {
throw new BuildException(String.format(
"For '%1$s' SDK Preview, attribute minSdkVersion in AndroidManifest.xml must be '%1$s' (current: %2$s)",
codeName, value));
}
// set the API level to the previous API level (which is actually the value in
// androidVersion.)
antProject.setProperty(mTargetApiOut,
Integer.toString(androidVersion.getApiLevel()));
} else if (value.length() > 0) {
// for normal platform, we'll only display warnings if the value is lower or higher
// than the target api level.
// First convert to an int.
int minSdkValue = -1;
try {
minSdkValue = Integer.parseInt(value);
} catch (NumberFormatException e) {
// looks like it's not a number: error!
throw new BuildException(String.format(
"Attribute %1$s in AndroidManifest.xml must be an Integer!",
AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION));
}
// set the target api to the value
antProject.setProperty(mTargetApiOut, value);
int projectApiLevel = androidVersion.getApiLevel();
if (minSdkValue > androidVersion.getApiLevel()) {
System.out.println(String.format(
"WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is higher than the project target API level (%3$d)",
AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
minSdkValue, projectApiLevel));
}
} else {
// no minSdkVersion? display a warning
System.out.println(
"WARNING: No minSdkVersion value set. Application will install on all Android versions.");
// set the target api to 1
antProject.setProperty(mTargetApiOut, "1");
}
} catch (XPathExpressionException e) {
throw new BuildException(e);
} catch (FileNotFoundException e) {
throw new BuildException(e);
}
}
}