/* * 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.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.IAndroidTarget.OptionalLibrary; import com.android.sdklib.SdkManager; import com.android.sdklib.internal.project.ProjectProperties; import com.android.utils.ILogger; import com.android.xml.AndroidManifest; import com.android.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 java.util.List; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; /** * Task to resolve the target of the current Android project. * * Out params: * <code>bootClassPathOut</code>: The boot class path of the project. * * <code>androidJarFileOut</code>: the android.jar used by the project. * * <code>androidAidlFileOut</code>: the framework.aidl used by the project. * * <code>targetApiOut</code>: the build API level. * * <code>minSdkVersionOut</code>: 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<String> messages = new ArrayList<String>(); SdkManager manager = SdkManager.createManager(sdkDir.getPath(), new ILogger() { @Override public void error(@Nullable Throwable t, @Nullable 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 info(@NonNull String msgFormat, Object... args) { messages.add(String.format(msgFormat, args)); } @Override public void verbose(@NonNull String msgFormat, Object... args) { info(msgFormat, args); } @Override public void warning(@NonNull 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(mTargetApiOut, 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. List<OptionalLibrary> libraries = androidTarget.getAdditionalLibraries(); HashSet<File> visitedJars = new HashSet<File>(); for (OptionalLibrary library : libraries) { File jarFile = library.getJar(); if (!visitedJars.contains(jarFile)) { visitedJars.add(jarFile); element = bootclasspath.createPathElement(); element.setPath(jarFile.getAbsolutePath()); } } // sets the path in the project with a reference antProject.addReference(mBootClassPathOut, bootclasspath); } /** * Checks the manifest <code>minSdkVersion</code> 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 minSdkVersion to the previous API level (which is actually the value in // androidVersion.) antProject.setProperty(mMinSdkVersionOut, Integer.toString(androidVersion.getApiLevel())); } else if (!value.isEmpty()) { // 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 minSdkVersion to the value antProject.setProperty(mMinSdkVersionOut, 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(mMinSdkVersionOut, "1"); } } catch (XPathExpressionException e) { throw new BuildException(e); } catch (FileNotFoundException e) { throw new BuildException(e); } } }