diff options
17 files changed, 850 insertions, 548 deletions
diff --git a/anttasks/src/com/android/ant/MultiApkExportTask.java b/anttasks/src/com/android/ant/MultiApkExportTask.java index f515076..dbc1a1d 100644 --- a/anttasks/src/com/android/ant/MultiApkExportTask.java +++ b/anttasks/src/com/android/ant/MultiApkExportTask.java @@ -16,14 +16,12 @@ package com.android.ant; -import com.android.sdklib.SdkConstants; import com.android.sdklib.internal.export.ApkData; -import com.android.sdklib.internal.project.ApkSettings; -import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.sdklib.internal.export.MultiApkExportHelper; +import com.android.sdklib.internal.export.MultiApkExportHelper.ExportException; +import com.android.sdklib.internal.export.MultiApkExportHelper.Target; import com.android.sdklib.io.FileWrapper; -import com.android.sdklib.io.StreamException; -import com.android.sdklib.xml.AndroidManifest; +import com.android.sdklib.io.IAbstractFile; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; @@ -34,18 +32,10 @@ import org.apache.tools.ant.taskdefs.SubAnt; import org.apache.tools.ant.types.FileSet; import org.xml.sax.InputSource; -import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.FilenameFilter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; -import java.util.List; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; @@ -58,37 +48,6 @@ import javax.xml.xpath.XPathFactory; */ public class MultiApkExportTask extends Task { - private final static int MAX_MINOR = 100; - private final static int MAX_BUILDINFO = 100; - private final static int OFFSET_BUILD_INFO = MAX_MINOR; - private final static int OFFSET_VERSION_CODE = OFFSET_BUILD_INFO * MAX_BUILDINFO; - - - private static enum Target { - RELEASE("release"), CLEAN("clean"); - - private final String mName; - - Target(String name) { - mName = name; - } - - String getTarget() { - return mName; - } - - static Target getTarget(String value) { - for (Target t : values()) { - if (t.mName.equals(value)) { - return t; - } - - } - - return null; - } - } - private Target mTarget; public void setTarget(String target) { @@ -134,170 +93,154 @@ public class MultiApkExportTask extends Task { canSign = keyStore != null && keyAlias != null; } - // get the list of apk to export and their configuration. - ApkData[] apks = getProjects(antProject, appPackage); + // get the list of projects + String projects = getValidatedProperty(antProject, "projects"); // look to see if there's an export log from a previous export - File log = getBuildLog(appPackage, versionCode); - if (mTarget == Target.RELEASE && log.isFile()) { - // load the log and compare to current export list. - // Any difference will force a new versionCode. - ApkData[] previousApks = getProjects(log); - - if (previousApks.length != apks.length) { - throw new BuildException(String.format( - "Project export is setup differently from previous export at versionCode %d.\n" + - "Any change in the multi-apk configuration require a increment of the versionCode.", - versionCode)); - } + IAbstractFile log = getBuildLog(appPackage, versionCode); - for (int i = 0 ; i < previousApks.length ; i++) { - // update the minor value from what is in the log file. - apks[i].setMinor(previousApks[i].getMinor()); - if (apks[i].compareTo(previousApks[i]) != 0) { - throw new BuildException(String.format( - "Project export is setup differently from previous export at versionCode %d.\n" + - "Any change in the multi-apk configuration require a increment of the versionCode.", - versionCode)); - } + MultiApkExportHelper helper = new MultiApkExportHelper(appPackage, versionCode, mTarget); + try { + ApkData[] apks = helper.getProjects(projects, log); + + // some temp var used by the project loop + HashSet<String> compiledProject = new HashSet<String>(); + XPathFactory xPathFactory = XPathFactory.newInstance(); + + File exportProjectOutput = new File(getValidatedProperty(antProject, "out.absolute.dir")); + + // if there's no error, and we can sign, prompt for the passwords. + String keyStorePassword = null; + String keyAliasPassword = null; + if (canSign) { + System.out.println("Found signing keystore and key alias. Need passwords."); + + Input input = new Input(); + input.setProject(antProject); + input.setAddproperty("key.store.password"); + input.setMessage(String.format("Please enter keystore password (store: %1$s):", + keyStore)); + input.execute(); + + input = new Input(); + input.setProject(antProject); + input.setAddproperty("key.alias.password"); + input.setMessage(String.format("Please enter password for alias '%1$s':", + keyAlias)); + input.execute(); + + // and now read the property so that they can be set into the sub ant task. + keyStorePassword = getValidatedProperty(antProject, "key.store.password"); + keyAliasPassword = getValidatedProperty(antProject, "key.alias.password"); } - } - - // some temp var used by the project loop - HashSet<String> compiledProject = new HashSet<String>(); - XPathFactory xPathFactory = XPathFactory.newInstance(); - - File exportProjectOutput = new File(getValidatedProperty(antProject, "out.absolute.dir")); - - // if there's no error, and we can sign, prompt for the passwords. - String keyStorePassword = null; - String keyAliasPassword = null; - if (canSign) { - System.out.println("Found signing keystore and key alias. Need passwords."); - - Input input = new Input(); - input.setProject(antProject); - input.setAddproperty("key.store.password"); - input.setMessage(String.format("Please enter keystore password (store: %1$s):", - keyStore)); - input.execute(); - - input = new Input(); - input.setProject(antProject); - input.setAddproperty("key.alias.password"); - input.setMessage(String.format("Please enter password for alias '%1$s':", - keyAlias)); - input.execute(); - - // and now read the property so that they can be set into the sub ant task. - keyStorePassword = getValidatedProperty(antProject, "key.store.password"); - keyAliasPassword = getValidatedProperty(antProject, "key.alias.password"); - } - - for (ApkData apk : apks) { - // this output is prepended by "[android-export] " (17 chars), so we put 61 stars - System.out.println("\n*************************************************************"); - System.out.println("Exporting project: " + apk.getRelativePath()); - - SubAnt subAnt = new SubAnt(); - subAnt.setTarget(mTarget.getTarget()); - subAnt.setProject(antProject); - - File subProjectFolder = new File(antProject.getBaseDir(), apk.getRelativePath()); - FileSet fileSet = new FileSet(); - fileSet.setProject(antProject); - fileSet.setDir(subProjectFolder); - fileSet.setIncludes("build.xml"); - subAnt.addFileset(fileSet); - -// subAnt.setVerbose(true); + for (ApkData apk : apks) { + // this output is prepended by "[android-export] " (17 chars), so we put 61 stars + System.out.println("\n*************************************************************"); + System.out.println("Exporting project: " + apk.getRelativePath()); + + SubAnt subAnt = new SubAnt(); + subAnt.setTarget(mTarget.getTarget()); + subAnt.setProject(antProject); + + File subProjectFolder = new File(antProject.getBaseDir(), apk.getRelativePath()); + + FileSet fileSet = new FileSet(); + fileSet.setProject(antProject); + fileSet.setDir(subProjectFolder); + fileSet.setIncludes("build.xml"); + subAnt.addFileset(fileSet); + + // subAnt.setVerbose(true); + + if (mTarget == Target.RELEASE) { + // only do the compilation part if it's the first time we export + // this project. + // (projects can be export multiple time if some properties are set up to + // generate more than one APK (for instance ABI split). + if (compiledProject.contains(apk.getRelativePath()) == false) { + compiledProject.add(apk.getRelativePath()); + } else { + addProp(subAnt, "do.not.compile", "true"); + } - if (mTarget == Target.RELEASE) { - // only do the compilation part if it's the first time we export - // this project. - // (projects can be export multiple time if some properties are set up to - // generate more than one APK (for instance ABI split). - if (compiledProject.contains(apk.getRelativePath()) == false) { - compiledProject.add(apk.getRelativePath()); - } else { - addProp(subAnt, "do.not.compile", "true"); - } + // set the version code, and filtering + String compositeVersionCode = getVersionCodeString(versionCode, apk); + addProp(subAnt, "version.code", compositeVersionCode); + System.out.println("Composite versionCode: " + compositeVersionCode); + String abi = apk.getAbi(); + if (abi != null) { + addProp(subAnt, "filter.abi", abi); + System.out.println("ABI Filter: " + abi); + } - // set the version code, and filtering - String compositeVersionCode = getVersionCodeString(versionCode, apk); - addProp(subAnt, "version.code", compositeVersionCode); - System.out.println("Composite versionCode: " + compositeVersionCode); - String abi = apk.getAbi(); - if (abi != null) { - addProp(subAnt, "filter.abi", abi); - System.out.println("ABI Filter: " + abi); - } + // end of the output by this task. Everything that follows will be output + // by the subant. + System.out.println("Calling to project's Ant file..."); + System.out.println("----------\n"); + + // set the output file names/paths. Keep all the temporary files in the project + // folder, and only put the final file (which is different depending on whether + // the file can be signed) locally. + + // read the base name from the build.xml file. + String name = null; + try { + File buildFile = new File(subProjectFolder, "build.xml"); + XPath xPath = xPathFactory.newXPath(); + name = xPath.evaluate("/project/@name", + new InputSource(new FileInputStream(buildFile))); + } catch (XPathExpressionException e) { + throw new BuildException("Failed to read build.xml", e); + } catch (FileNotFoundException e) { + throw new BuildException("build.xml is missing.", e); + } - // end of the output by this task. Everything that follows will be output - // by the subant. - System.out.println("Calling to project's Ant file..."); - System.out.println("----------\n"); - - // set the output file names/paths. Keep all the temporary files in the project - // folder, and only put the final file (which is different depending on whether - // the file can be signed) locally. - - // read the base name from the build.xml file. - String name = null; - try { - File buildFile = new File(subProjectFolder, "build.xml"); - XPath xPath = xPathFactory.newXPath(); - name = xPath.evaluate("/project/@name", - new InputSource(new FileInputStream(buildFile))); - } catch (XPathExpressionException e) { - throw new BuildException("Failed to read build.xml", e); - } catch (FileNotFoundException e) { - throw new BuildException("build.xml is missing.", e); + // override the resource pack file. + addProp(subAnt, "resource.package.file.name", + name + "-" + apk.getBuildInfo() + ".ap_"); + + if (canSign) { + // set the properties for the password. + addProp(subAnt, "key.store", keyStore); + addProp(subAnt, "key.alias", keyAlias); + addProp(subAnt, "key.store.password", keyStorePassword); + addProp(subAnt, "key.alias.password", keyAliasPassword); + + // temporary file only get a filename change (still stored in the project + // bin folder). + addProp(subAnt, "out.unsigned.file.name", + name + "-" + apk.getBuildInfo() + "-unsigned.apk"); + addProp(subAnt, "out.unaligned.file", + name + "-" + apk.getBuildInfo() + "-unaligned.apk"); + + // final file is stored locally. + apk.setOutputName(name + "-" + compositeVersionCode + "-release.apk"); + addProp(subAnt, "out.release.file", new File(exportProjectOutput, + apk.getOutputName()).getAbsolutePath()); + + } else { + // put some empty prop. This is to override possible ones defined in the + // project. The reason is that if there's more than one project, we don't + // want some to signed and some not to be (and we don't want each project + // to prompt for password.) + addProp(subAnt, "key.store", ""); + addProp(subAnt, "key.alias", ""); + // final file is the unsigned version. It gets stored locally. + apk.setOutputName(name + "-" + compositeVersionCode + "-unsigned.apk"); + addProp(subAnt, "out.unsigned.file", new File(exportProjectOutput, + apk.getOutputName()).getAbsolutePath()); + } } - // override the resource pack file. - addProp(subAnt, "resource.package.file.name", - name + "-" + apk.getBuildInfo() + ".ap_"); - - if (canSign) { - // set the properties for the password. - addProp(subAnt, "key.store", keyStore); - addProp(subAnt, "key.alias", keyAlias); - addProp(subAnt, "key.store.password", keyStorePassword); - addProp(subAnt, "key.alias.password", keyAliasPassword); - - // temporary file only get a filename change (still stored in the project - // bin folder). - addProp(subAnt, "out.unsigned.file.name", - name + "-" + apk.getBuildInfo() + "-unsigned.apk"); - addProp(subAnt, "out.unaligned.file", - name + "-" + apk.getBuildInfo() + "-unaligned.apk"); - - // final file is stored locally. - apk.setOutputName(name + "-" + compositeVersionCode + "-release.apk"); - addProp(subAnt, "out.release.file", new File(exportProjectOutput, - apk.getOutputName()).getAbsolutePath()); - - } else { - // put some empty prop. This is to override possible ones defined in the - // project. The reason is that if there's more than one project, we don't - // want some to signed and some not to be (and we don't want each project - // to prompt for password.) - addProp(subAnt, "key.store", ""); - addProp(subAnt, "key.alias", ""); - // final file is the unsigned version. It gets stored locally. - apk.setOutputName(name + "-" + compositeVersionCode + "-unsigned.apk"); - addProp(subAnt, "out.unsigned.file", new File(exportProjectOutput, - apk.getOutputName()).getAbsolutePath()); - } + subAnt.execute(); } - subAnt.execute(); - } - - if (mTarget == Target.RELEASE) { - makeBuildLog(appPackage, versionCode, apks); + if (mTarget == Target.RELEASE) { + helper.makeBuildLog(log, apks); + } + } catch (ExportException e) { + throw new BuildException(e); } } @@ -318,174 +261,6 @@ public class MultiApkExportTask extends Task { return value; } - /** - * gets the projects to export from the property, checks they exist, validates them, - * loads their export info and return it. - * If a project does not exist or is not valid, this will throw a {@link BuildException}. - * @param antProject the Ant project. - * @param appPackage the application package. Projects' manifest must match this. - */ - private ApkData[] getProjects(Project antProject, String appPackage) { - String projects = antProject.getProperty("projects"); - String[] paths = projects.split("\\:"); - - ArrayList<ApkData> datalist = new ArrayList<ApkData>(); - - for (String path : paths) { - File projectFolder = new File(path); - - // resolve the path (to remove potential ..) - try { - projectFolder = projectFolder.getCanonicalFile(); - - // project folder must exist and be a directory - if (projectFolder.isDirectory() == false) { - throw new BuildException(String.format( - "Project folder '%1$s' is not a valid directory.", - projectFolder.getAbsolutePath())); - } - - // Check AndroidManifest.xml is present - FileWrapper androidManifest = new FileWrapper(projectFolder, - SdkConstants.FN_ANDROID_MANIFEST_XML); - - if (androidManifest.isFile() == false) { - throw new BuildException(String.format( - "%1$s is not a valid project (%2$s not found).", - projectFolder.getAbsolutePath(), - SdkConstants.FN_ANDROID_MANIFEST_XML)); - } - - ArrayList<ApkData> datalist2 = checkManifest(androidManifest, appPackage); - - // if the method returns without throwing, this is a good project to - // export. - for (ApkData data : datalist2) { - data.setRelativePath(path); - data.setProject(projectFolder); - } - - datalist.addAll(datalist2); - - } catch (IOException e) { - throw new BuildException(String.format("Failed to resolve path %1$s", path), e); - } - } - - // sort the projects and assign buildInfo - Collections.sort(datalist); - int buildInfo = 0; - for (ApkData data : datalist) { - data.setBuildInfo(buildInfo++); - } - - return datalist.toArray(new ApkData[datalist.size()]); - } - - /** - * Checks a manifest of the project for inclusion in the list of exported APK. - * If the manifest is correct, a list of apk to export is created and returned. - * - * @param androidManifest the manifest to check - * @param appPackage the package name of the application being exported, as read from - * export.properties. - * @return A new non-null {@link ArrayList} of {@link ApkData}. - * - * @throws BuildException in case of error. - */ - private ArrayList<ApkData> checkManifest(FileWrapper androidManifest, String appPackage) { - try { - String manifestPackage = AndroidManifest.getPackage(androidManifest); - if (appPackage.equals(manifestPackage) == false) { - throw new BuildException(String.format( - "%1$s package value is not valid. Found '%2$s', expected '%3$s'.", - androidManifest.getPath(), manifestPackage, appPackage)); - } - - if (AndroidManifest.hasVersionCode(androidManifest)) { - throw new BuildException(String.format( - "%1$s is not valid: versionCode must not be set for multi-apk export.", - androidManifest.getPath())); - } - - int minSdkVersion = AndroidManifest.getMinSdkVersion(androidManifest); - if (minSdkVersion == -1) { - throw new BuildException( - "Codename in minSdkVersion is not supported by multi-apk export."); - } - - ArrayList<ApkData> dataList = new ArrayList<ApkData>(); - ApkData data = new ApkData(); - dataList.add(data); - data.setMinSdkVersion(minSdkVersion); - - // only look for more exports if the target is not clean. - if (mTarget != Target.CLEAN) { - // load the project properties - String projectPath = androidManifest.getParent(); - ProjectProperties projectProp = ProjectProperties.load(projectPath, - PropertyType.DEFAULT); - if (projectProp == null) { - throw new BuildException(String.format( - "%1$s is missing.", PropertyType.DEFAULT.getFilename())); - } - - ApkSettings apkSettings = new ApkSettings(projectProp); - if (apkSettings.isSplitByAbi()) { - // need to find the available ABIs. - List<String> abis = findAbis(projectPath); - ApkData current = data; - for (String abi : abis) { - if (current == null) { - current = new ApkData(data); - dataList.add(current); - } - - current.setAbi(abi); - current = null; - } - } - } - - return dataList; - } catch (XPathExpressionException e) { - throw new BuildException( - String.format("Failed to validate %1$s", androidManifest.getPath()), e); - } catch (StreamException e) { - throw new BuildException( - String.format("Failed to validate %1$s", androidManifest.getPath()), e); - } - } - - /** - * Finds ABIs in a project folder. This is based on the presence of libs/<abi>/ folder. - * - * @param projectPath The OS path of the project. - * @return A new non-null, possibly empty, list of ABI strings. - */ - private List<String> findAbis(String projectPath) { - ArrayList<String> abiList = new ArrayList<String>(); - File libs = new File(projectPath, SdkConstants.FD_NATIVE_LIBS); - if (libs.isDirectory()) { - File[] abis = libs.listFiles(); - for (File abi : abis) { - if (abi.isDirectory()) { - // only add the abi folder if there are .so files in it. - String[] content = abi.list(new FilenameFilter() { - public boolean accept(File dir, String name) { - return name.toLowerCase().endsWith(".so"); - } - }); - - if (content.length > 0) { - abiList.add(abi.getName()); - } - } - } - } - - return abiList; - } /** * Adds a property to a {@link SubAnt} task. @@ -507,8 +282,8 @@ public class MultiApkExportTask extends Task { * @return the composite versionCode to be used in the manifest. */ private String getVersionCodeString(int versionCode, ApkData apkData) { - int trueVersionCode = versionCode * OFFSET_VERSION_CODE; - trueVersionCode += apkData.getBuildInfo() * OFFSET_BUILD_INFO; + int trueVersionCode = versionCode * MultiApkExportHelper.OFFSET_VERSION_CODE; + trueVersionCode += apkData.getBuildInfo() * MultiApkExportHelper.OFFSET_BUILD_INFO; trueVersionCode += apkData.getMinor(); return Integer.toString(trueVersionCode); @@ -518,119 +293,9 @@ public class MultiApkExportTask extends Task { * Returns the {@link File} for the build log. * @param appPackage * @param versionCode - * @return A new non-null {@link File} mapping to the build log. + * @return A new non-null {@link IAbstractFile} mapping to the build log. */ - private File getBuildLog(String appPackage, int versionCode) { - return new File(appPackage + "." + versionCode + ".log"); + private IAbstractFile getBuildLog(String appPackage, int versionCode) { + return new FileWrapper(appPackage + "." + versionCode + ".log"); } - - /** - * Loads and returns a list of {@link ApkData} from a build log. - * @param log - * @return A new non-null, possibly empty, array of {@link ApkData}. - * @throws BuildException in case of error. - */ - private ApkData[] getProjects(File log) { - ArrayList<ApkData> datalist = new ArrayList<ApkData>(); - - FileReader reader = null; - BufferedReader bufferedReader = null; - try { - reader = new FileReader(log); - bufferedReader = new BufferedReader(reader); - String line; - int lineIndex = 0; - int apkIndex = 0; - while ((line = bufferedReader.readLine()) != null) { - line = line.trim(); - if (line.length() == 0 || line.startsWith("#")) { - continue; - } - - switch (lineIndex) { - case 0: - // read package value - lineIndex++; - break; - case 1: - // read versionCode value - lineIndex++; - break; - default: - // read apk description - ApkData data = new ApkData(); - data.setBuildInfo(apkIndex++); - datalist.add(data); - data.read(line); - if (data.getMinor() >= MAX_MINOR) { - throw new BuildException( - "Valid minor version code values are 0-" + (MAX_MINOR-1)); - } - break; - } - } - } catch (IOException e) { - throw new BuildException("Failed to read existing build log", e); - } finally { - try { - if (reader != null) { - reader.close(); - } - } catch (IOException e) { - throw new BuildException("Failed to read existing build log", e); - } - } - - return datalist.toArray(new ApkData[datalist.size()]); - } - - /** - * Writes the build log for a given list of {@link ApkData}. - * @param appPackage - * @param versionCode - * @param apks - */ - private void makeBuildLog(String appPackage, int versionCode, ApkData[] apks) { - File log = getBuildLog(appPackage, versionCode); - FileWriter writer = null; - try { - writer = new FileWriter(log); - - writer.append("# Multi-APK BUILD log.\n"); - writer.append("# Only edit manually to change minor versions.\n"); - - writeValue(writer, "package", appPackage); - writeValue(writer, "versionCode", versionCode); - - writer.append("# what follows is one line per generated apk with its description.\n"); - writer.append("# the format is CSV in the following order:\n"); - writer.append("# apkname,project,minor, minsdkversion, abi filter,\n"); - - for (ApkData apk : apks) { - apk.write(writer); - writer.append('\n'); - } - - writer.flush(); - } catch (IOException e) { - throw new BuildException("Failed to write build log", e); - } finally { - try { - if (writer != null) { - writer.close(); - } - } catch (IOException e) { - throw new BuildException("Failed to write build log", e); - } - } - } - - private void writeValue(FileWriter writer, String name, String value) throws IOException { - writer.append(name).append('=').append(value).append('\n'); - } - - private void writeValue(FileWriter writer, String name, int value) throws IOException { - writeValue(writer, name, Integer.toString(value)); - } - } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectState.java index 2a55f74..93c77f9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectState.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ProjectState.java @@ -494,19 +494,24 @@ public final class ProjectState { /** * Saves the default.properties file and refreshes it to make sure that it gets reloaded * by Eclipse + * @throws Exception */ - public void saveProperties() { + public void saveProperties() throws CoreException { try { mProperties.save(); IResource defaultProp = mProject.findMember(SdkConstants.FN_DEFAULT_PROPERTIES); defaultProp.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (CoreException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + } catch (Exception e) { + String msg = String.format( + "Failed to save %1$s for project %2$s", + SdkConstants.FN_DEFAULT_PROPERTIES, mProject.getName()); + AdtPlugin.log(e, msg); + if (e instanceof CoreException) { + throw (CoreException)e; + } else { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, msg, e)); + } } } @@ -524,11 +529,12 @@ public final class ProjectState { mProperties.setProperty(propName, newValue); try { mProperties.save(); - } catch (IOException e) { - return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, - String.format("Failed to save %1$s for project %2$s", - mProperties.getType().getFilename(), mProject.getName()), + } catch (Exception e) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format( + "Failed to save %1$s for project %2$s", + mProperties.getType() .getFilename(), mProject.getName()), e); + } return Status.OK_STATUS; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java index f31e318..4621431 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/properties/AndroidPropertyPage.java @@ -23,6 +23,7 @@ import com.android.sdklib.internal.project.ProjectProperties; import com.android.sdkuilib.internal.widgets.SdkTargetSelector; import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; @@ -155,7 +156,11 @@ public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPrope // TODO: update ApkSettings. if (mustSaveProp) { - state.saveProperties(); + try { + state.saveProperties(); + } catch (CoreException e) { + // pass + } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java index 76c057e..2503185 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java @@ -36,6 +36,7 @@ import com.android.sdklib.SdkManager; import com.android.sdklib.internal.avd.AvdManager; import com.android.sdklib.internal.project.ProjectProperties; import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.sdklib.io.StreamException; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; @@ -289,8 +290,10 @@ public final class Sdk { * @param project the project to intialize * @param target the project's target. * @throws IOException if creating the file failed in any way. + * @throws StreamException */ - public void initProject(IProject project, IAndroidTarget target) throws IOException { + public void initProject(IProject project, IAndroidTarget target) + throws IOException, StreamException { if (project == null || target == null) { return; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java index 901bafe..89baa1c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java @@ -25,6 +25,7 @@ import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreatio import com.android.ide.eclipse.adt.internal.wizards.newproject.NewTestProjectCreationPage.TestInfo; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkConstants; +import com.android.sdklib.io.StreamException; import com.android.sdklib.resources.Density; import org.eclipse.core.resources.IContainer; @@ -592,6 +593,8 @@ public class NewProjectWizard extends Wizard implements INewWizard { throw new InvocationTargetException(e); } catch (IOException e) { throw new InvocationTargetException(e); + } catch (StreamException e) { + throw new InvocationTargetException(e); } finally { monitor.done(); } @@ -607,13 +610,14 @@ public class NewProjectWizard extends Wizard implements INewWizard { * @param parameters Template parameters. * @param dictionary String definition. * @return The project newly created + * @throws StreamException */ private IProject createEclipseProject(IProgressMonitor monitor, IProject project, IProjectDescription description, Map<String, Object> parameters, Map<String, String> dictionary) - throws CoreException, IOException { + throws CoreException, IOException, StreamException { // get the project target IAndroidTarget target = (IAndroidTarget) parameters.get(PARAM_SDK_TARGET); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/io/IFileWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/io/IFileWrapper.java index 4b96789..dc022c7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/io/IFileWrapper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/io/IFileWrapper.java @@ -17,13 +17,19 @@ package com.android.ide.eclipse.adt.io; import com.android.sdklib.io.IAbstractFile; +import com.android.sdklib.io.IAbstractFolder; import com.android.sdklib.io.StreamException; +import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; /** * An implementation of {@link IAbstractFile} on top of an {@link IFile} object. @@ -52,6 +58,26 @@ public class IFileWrapper implements IAbstractFile { } } + public OutputStream getOutputStream() throws StreamException { + return new ByteArrayOutputStream() { + @Override + public void close() throws IOException { + super.close(); + + byte[] data = toByteArray(); + try { + setContents(new ByteArrayInputStream(data)); + } catch (StreamException e) { + throw new IOException(); + } + } + }; + } + + public PreferredWriteMode getPreferredWriteMode() { + return PreferredWriteMode.INPUTSTREAM; + } + public String getOsLocation() { return mFile.getLocation().toOSString(); } @@ -88,4 +114,13 @@ public class IFileWrapper implements IAbstractFile { public int hashCode() { return mFile.hashCode(); } + + public IAbstractFolder getParentFolder() { + IContainer p = mFile.getParent(); + if (p != null) { + return new IFolderWrapper(p); + } + + return null; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/io/IFolderWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/io/IFolderWrapper.java index 158fd8b..265ea33 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/io/IFolderWrapper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/io/IFolderWrapper.java @@ -27,6 +27,8 @@ import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.Path; +import java.util.ArrayList; + /** * An implementation of {@link IAbstractFolder} on top of either an {@link IFolder} or an * {@link IContainer} object. @@ -126,4 +128,47 @@ public class IFolderWrapper implements IAbstractFolder { public int hashCode() { return mContainer.hashCode(); } + + public IAbstractFolder getFolder(String name) { + if (mFolder != null) { + IFolder folder = mFolder.getFolder(name); + return new IFolderWrapper(folder); + } + + IFolder folder = mContainer.getFolder(new Path(name)); + return new IFolderWrapper(folder); + } + + public String getOsLocation() { + return mContainer.getLocation().toOSString(); + } + + public String[] list(FilenameFilter filter) { + try { + IResource[] members = mContainer.members(); + if (members.length > 0) { + ArrayList<String> list = new ArrayList<String>(); + for (IResource res : members) { + if (filter.accept(this, res.getName())) { + list.add(res.getName()); + } + } + + return list.toArray(new String[list.size()]); + } + } catch (CoreException e) { + // can't read the members? return empty list below. + } + + return new String[0]; + } + + public IAbstractFolder getParentFolder() { + IContainer p = mContainer.getParent(); + if (p != null) { + return new IFolderWrapper(p); + } + + return null; + } } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java index 519e8fb..71fcee5 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java @@ -19,6 +19,9 @@ package com.android.sdklib; import com.android.prefs.AndroidLocation; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.AndroidVersion.AndroidVersionException; +import com.android.sdklib.io.FileWrapper; +import com.android.sdklib.io.IAbstractFile; +import com.android.sdklib.io.StreamException; import java.io.BufferedReader; import java.io.File; @@ -613,13 +616,26 @@ public final class SdkManager { * @param propFile the property file to parse * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null. * @return the map of (key,value) pairs, or null if the parsing failed. + * @deprecated Use {@link #parsePropertyFile(IAbstractFile, ISdkLog)} */ public static Map<String, String> parsePropertyFile(File propFile, ISdkLog log) { - FileInputStream fis = null; + IAbstractFile wrapper = new FileWrapper(propFile); + return parsePropertyFile(wrapper, log); + } + + /** + * Parses a property file (using UTF-8 encoding) and returns a map of the content. + * <p/>If the file is not present, null is returned with no error messages sent to the log. + * + * @param propFile the property file to parse + * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + public static Map<String, String> parsePropertyFile(IAbstractFile propFile, ISdkLog log) { BufferedReader reader = null; try { - fis = new FileInputStream(propFile); - reader = new BufferedReader(new InputStreamReader(fis, SdkConstants.INI_CHARSET)); + reader = new BufferedReader(new InputStreamReader(propFile.getContents(), + SdkConstants.INI_CHARSET)); String line = null; Map<String, String> map = new HashMap<String, String>(); @@ -631,7 +647,7 @@ public final class SdkManager { map.put(m.group(1), m.group(2)); } else { log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", - propFile.getAbsolutePath(), + propFile.getOsLocation(), line); return null; } @@ -645,7 +661,11 @@ public final class SdkManager { // Return null below. } catch (IOException e) { log.warning("Error parsing '%1$s': %2$s.", - propFile.getAbsolutePath(), + propFile.getOsLocation(), + e.getMessage()); + } catch (StreamException e) { + log.warning("Error parsing '%1$s': %2$s.", + propFile.getOsLocation(), e.getMessage()); } finally { if (reader != null) { @@ -655,13 +675,6 @@ public final class SdkManager { // pass } } - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - // pass - } - } } return null; diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/export/ApkData.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/export/ApkData.java index 50acebc..9c3a8c5 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/export/ApkData.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/export/ApkData.java @@ -19,6 +19,7 @@ package com.android.sdklib.internal.export; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.OutputStreamWriter; /** * Class representing one apk that needs to be generated. This contains @@ -174,12 +175,12 @@ public class ApkData implements Comparable<ApkData> { /** * Writes the apk description in the given writer. a single line is used to write * everything. - * @param writer The {@link FileWriter} to write to. + * @param writer The {@link OutputStreamWriter} to write to. * @throws IOException * * @see {@link #read(String)} */ - public void write(FileWriter writer) throws IOException { + public void write(OutputStreamWriter writer) throws IOException { for (int i = 0 ; i < ApkData.INDEX_MAX ; i++) { write(i, writer); } @@ -198,7 +199,7 @@ public class ApkData implements Comparable<ApkData> { } } - private void write(int index, FileWriter writer) throws IOException { + private void write(int index, OutputStreamWriter writer) throws IOException { switch (index) { case INDEX_OUTPUTNAME: writeValue(writer, mOutputName); @@ -240,11 +241,11 @@ public class ApkData implements Comparable<ApkData> { } } - private static void writeValue(FileWriter writer, String value) throws IOException { + private static void writeValue(OutputStreamWriter writer, String value) throws IOException { writer.append(value).append(','); } - private static void writeValue(FileWriter writer, int value) throws IOException { + private static void writeValue(OutputStreamWriter writer, int value) throws IOException { writeValue(writer, Integer.toString(value)); } } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/export/MultiApkExportHelper.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/export/MultiApkExportHelper.java new file mode 100644 index 0000000..72014e4 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/export/MultiApkExportHelper.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2010 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.internal.export; + +import com.android.sdklib.SdkConstants; +import com.android.sdklib.internal.project.ApkSettings; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.sdklib.io.FileWrapper; +import com.android.sdklib.io.IAbstractFile; +import com.android.sdklib.io.IAbstractFolder; +import com.android.sdklib.io.IAbstractResource; +import com.android.sdklib.io.StreamException; +import com.android.sdklib.io.IAbstractFolder.FilenameFilter; +import com.android.sdklib.xml.AndroidManifest; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.xml.xpath.XPathExpressionException; + +public class MultiApkExportHelper { + + private final String mAppPackage; + private final int mVersionCode; + private final Target mTarget; + + public final static int MAX_MINOR = 100; + public final static int MAX_BUILDINFO = 100; + public final static int OFFSET_BUILD_INFO = MAX_MINOR; + public final static int OFFSET_VERSION_CODE = OFFSET_BUILD_INFO * MAX_BUILDINFO; + + public static final class ExportException extends Exception { + private static final long serialVersionUID = 1L; + + public ExportException(String message) { + super(message); + } + + public ExportException(String message, Throwable cause) { + super(message, cause); + } + } + + public static enum Target { + RELEASE("release"), CLEAN("clean"); + + private final String mName; + + Target(String name) { + mName = name; + } + + public String getTarget() { + return mName; + } + + public static Target getTarget(String value) { + for (Target t : values()) { + if (t.mName.equals(value)) { + return t; + } + + } + + return null; + } + } + + public MultiApkExportHelper(String appPackage, int versionCode, Target target) { + mAppPackage = appPackage; + mVersionCode = versionCode; + mTarget = target; + } + + public ApkData[] getProjects(String projects, IAbstractFile buildLog) throws ExportException { + // get the list of apk to export and their configuration. + ApkData[] apks = getProjects(projects); + + // look to see if there's an export log from a previous export + if (mTarget == Target.RELEASE && buildLog != null && buildLog.exists()) { + // load the log and compare to current export list. + // Any difference will force a new versionCode. + ApkData[] previousApks = getProjects(buildLog); + + if (previousApks.length != apks.length) { + throw new ExportException(String.format( + "Project export is setup differently from previous export at versionCode %d.\n" + + "Any change in the multi-apk configuration requires an increment of the versionCode.", + mVersionCode)); + } + + for (int i = 0 ; i < previousApks.length ; i++) { + // update the minor value from what is in the log file. + apks[i].setMinor(previousApks[i].getMinor()); + if (apks[i].compareTo(previousApks[i]) != 0) { + throw new ExportException(String.format( + "Project export is setup differently from previous export at versionCode %d.\n" + + "Any change in the multi-apk configuration requires an increment of the versionCode.", + mVersionCode)); + } + } + } + + return apks; + + } + + /** + * Writes the build log for a given list of {@link ApkData}. + * @param buildLog the build log file into which to write the log. + * @param apks the list of apks that were exported. + * @throws ExportException + */ + public void makeBuildLog(IAbstractFile buildLog, ApkData[] apks) throws ExportException { + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(buildLog.getOutputStream()); + + writer.append("# Multi-APK BUILD log.\n"); + writer.append("# Only edit manually to change minor versions.\n"); + + writeValue(writer, "package", mAppPackage); + writeValue(writer, "versionCode", mVersionCode); + + writer.append("# what follows is one line per generated apk with its description.\n"); + writer.append("# the format is CSV in the following order:\n"); + writer.append("# apkname,project,minor, minsdkversion, abi filter,\n"); + + for (ApkData apk : apks) { + apk.write(writer); + writer.append('\n'); + } + + writer.flush(); + } catch (Exception e) { + throw new ExportException("Failed to write build log", e); + } finally { + try { + if (writer != null) { + writer.close(); + } + } catch (IOException e) { + throw new ExportException("Failed to write build log", e); + } + } + } + + private void writeValue(OutputStreamWriter writer, String name, String value) + throws IOException { + writer.append(name).append('=').append(value).append('\n'); + } + + private void writeValue(OutputStreamWriter writer, String name, int value) throws IOException { + writeValue(writer, name, Integer.toString(value)); + } + + /** + * gets the projects to export from the property, checks they exist, validates them, + * loads their export info and return it. + * If a project does not exist or is not valid, this will throw a {@link BuildException}. + * @param projects the Ant project. + * @throws ExportException + */ + private ApkData[] getProjects(String projects) throws ExportException { + String[] paths = projects.split("\\:"); + + ArrayList<ApkData> datalist = new ArrayList<ApkData>(); + + for (String path : paths) { + File projectFolder = new File(path); + + // resolve the path (to remove potential ..) + try { + projectFolder = projectFolder.getCanonicalFile(); + + // project folder must exist and be a directory + if (projectFolder.isDirectory() == false) { + throw new ExportException(String.format( + "Project folder '%1$s' is not a valid directory.", + projectFolder.getAbsolutePath())); + } + + // Check AndroidManifest.xml is present + FileWrapper androidManifest = new FileWrapper(projectFolder, + SdkConstants.FN_ANDROID_MANIFEST_XML); + + if (androidManifest.isFile() == false) { + throw new ExportException(String.format( + "%1$s is not a valid project (%2$s not found).", + projectFolder.getAbsolutePath(), + SdkConstants.FN_ANDROID_MANIFEST_XML)); + } + + ArrayList<ApkData> datalist2 = checkManifest(androidManifest); + + // if the method returns without throwing, this is a good project to + // export. + for (ApkData data : datalist2) { + data.setRelativePath(path); + data.setProject(projectFolder); + } + + datalist.addAll(datalist2); + + } catch (IOException e) { + throw new ExportException(String.format("Failed to resolve path %1$s", path), e); + } + } + + // sort the projects and assign buildInfo + Collections.sort(datalist); + int buildInfo = 0; + for (ApkData data : datalist) { + data.setBuildInfo(buildInfo++); + } + + return datalist.toArray(new ApkData[datalist.size()]); + } + + /** + * Checks a manifest of the project for inclusion in the list of exported APK. + * If the manifest is correct, a list of apk to export is created and returned. + * + * @param androidManifest the manifest to check + * @return A new non-null {@link ArrayList} of {@link ApkData}. + * @throws ExportException in case of error. + */ + private ArrayList<ApkData> checkManifest(IAbstractFile androidManifest) throws ExportException { + try { + String manifestPackage = AndroidManifest.getPackage(androidManifest); + if (mAppPackage.equals(manifestPackage) == false) { + throw new ExportException(String.format( + "%1$s package value is not valid. Found '%2$s', expected '%3$s'.", + androidManifest.getOsLocation(), manifestPackage, mAppPackage)); + } + + if (AndroidManifest.hasVersionCode(androidManifest)) { + throw new ExportException(String.format( + "%1$s is not valid: versionCode must not be set for multi-apk export.", + androidManifest.getOsLocation())); + } + + int minSdkVersion = AndroidManifest.getMinSdkVersion(androidManifest); + if (minSdkVersion == -1) { + throw new ExportException( + "Codename in minSdkVersion is not supported by multi-apk export."); + } + + ArrayList<ApkData> dataList = new ArrayList<ApkData>(); + ApkData data = new ApkData(); + dataList.add(data); + data.setMinSdkVersion(minSdkVersion); + + // only look for more exports if the target is not clean. + if (mTarget != Target.CLEAN) { + // load the project properties + IAbstractFolder projectFolder = androidManifest.getParentFolder(); + ProjectProperties projectProp = ProjectProperties.load(projectFolder, + PropertyType.DEFAULT); + if (projectProp == null) { + throw new ExportException(String.format( + "%1$s is missing.", PropertyType.DEFAULT.getFilename())); + } + + ApkSettings apkSettings = new ApkSettings(projectProp); + if (apkSettings.isSplitByAbi()) { + // need to find the available ABIs. + List<String> abis = findAbis(projectFolder); + ApkData current = data; + for (String abi : abis) { + if (current == null) { + current = new ApkData(data); + dataList.add(current); + } + + current.setAbi(abi); + current = null; + } + } + } + + return dataList; + } catch (XPathExpressionException e) { + throw new ExportException( + String.format("Failed to validate %1$s", androidManifest.getOsLocation()), e); + } catch (StreamException e) { + throw new ExportException( + String.format("Failed to validate %1$s", androidManifest.getOsLocation()), e); + } + } + + /** + * Loads and returns a list of {@link ApkData} from a build log. + * @param log + * @return A new non-null, possibly empty, array of {@link ApkData}. + * @throws ExportException + * @throws BuildException in case of error. + */ + private ApkData[] getProjects(IAbstractFile buildLog) throws ExportException { + ArrayList<ApkData> datalist = new ArrayList<ApkData>(); + + InputStreamReader reader = null; + BufferedReader bufferedReader = null; + try { + reader = new InputStreamReader(buildLog.getContents()); + bufferedReader = new BufferedReader(reader); + String line; + int lineIndex = 0; + int apkIndex = 0; + while ((line = bufferedReader.readLine()) != null) { + line = line.trim(); + if (line.length() == 0 || line.startsWith("#")) { + continue; + } + + switch (lineIndex) { + case 0: + // read package value + lineIndex++; + break; + case 1: + // read versionCode value + lineIndex++; + break; + default: + // read apk description + ApkData data = new ApkData(); + data.setBuildInfo(apkIndex++); + datalist.add(data); + data.read(line); + if (data.getMinor() >= MAX_MINOR) { + throw new ExportException( + "Valid minor version code values are 0-" + (MAX_MINOR-1)); + } + break; + } + } + } catch (IOException e) { + throw new ExportException("Failed to read existing build log", e); + } catch (StreamException e) { + throw new ExportException("Failed to read existing build log", e); + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + throw new ExportException("Failed to read existing build log", e); + } + } + + return datalist.toArray(new ApkData[datalist.size()]); + } + + /** + * Finds ABIs in a project folder. This is based on the presence of libs/<abi>/ folder. + * + * @param projectPath The OS path of the project. + * @return A new non-null, possibly empty, list of ABI strings. + */ + private List<String> findAbis(IAbstractFolder projectFolder) { + ArrayList<String> abiList = new ArrayList<String>(); + IAbstractFolder libs = projectFolder.getFolder(SdkConstants.FD_NATIVE_LIBS); + if (libs.exists()) { + IAbstractResource[] abis = libs.listMembers(); + for (IAbstractResource abi : abis) { + if (abi instanceof IAbstractFolder && abi.exists()) { + // only add the abi folder if there are .so files in it. + String[] content = ((IAbstractFolder)abi).list(new FilenameFilter() { + public boolean accept(IAbstractFolder dir, String name) { + return name.toLowerCase().endsWith(".so"); + } + }); + + if (content.length > 0) { + abiList.add(abi.getName()); + } + } + } + } + + return abiList; + } + + +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectCreator.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectCreator.java index 7b22e3d..ff32a6c 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectCreator.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectCreator.java @@ -391,9 +391,7 @@ public class ProjectCreator { installTemplate("build.template", new File(projectFolder, SdkConstants.FN_BUILD_XML), keywords); - } catch (ProjectCreateException e) { - mLog.error(e, null); - } catch (IOException e) { + } catch (Exception e) { mLog.error(e, null); } } @@ -517,7 +515,7 @@ public class ProjectCreator { try { props.save(); println("Updated %1$s", PropertyType.DEFAULT.getFilename()); - } catch (IOException e) { + } catch (Exception e) { mLog.error(e, "Failed to write %1$s file in '%2$s'", PropertyType.DEFAULT.getFilename(), folderPath); @@ -538,7 +536,7 @@ public class ProjectCreator { try { props.save(); println("Updated %1$s", PropertyType.LOCAL.getFilename()); - } catch (IOException e) { + } catch (Exception e) { mLog.error(e, "Failed to write %1$s file in '%2$s'", PropertyType.LOCAL.getFilename(), folderPath); @@ -718,7 +716,7 @@ public class ProjectCreator { try { buildProps.save(); println("Updated %1$s", PropertyType.BUILD.getFilename()); - } catch (IOException e) { + } catch (Exception e) { mLog.error(e, "Failed to write %1$s file in '%2$s'", PropertyType.BUILD.getFilename(), folderPath); diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java index 734efda..d05c9f6 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java @@ -18,9 +18,11 @@ package com.android.sdklib.internal.project; import com.android.sdklib.SdkConstants; import com.android.sdklib.SdkManager; +import com.android.sdklib.io.FolderWrapper; +import com.android.sdklib.io.IAbstractFile; +import com.android.sdklib.io.IAbstractFolder; +import com.android.sdklib.io.StreamException; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.HashMap; @@ -129,7 +131,7 @@ public final class ProjectProperties { "# Used by the 'uninstall' rule.\n"); } - private final String mProjectFolderOsPath; + private final IAbstractFolder mProjectFolder; private final Map<String, String> mProperties; private final PropertyType mType; @@ -141,13 +143,24 @@ public final class ProjectProperties { * @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 */); + IAbstractFolder wrapper = new FolderWrapper(projectFolderOsPath); + return load(wrapper, type); + } + + /** + * Loads a project properties file and return a {@link ProjectProperties} object + * containing the properties + * + * @param projectFolder the project folder. + * @param type One the possible {@link PropertyType}s. + */ + public static ProjectProperties load(IAbstractFolder projectFolder, PropertyType type) { + if (projectFolder.exists()) { + IAbstractFile propFile = projectFolder.getFile(type.mFilename); + if (propFile.exists()) { + Map<String, String> map = SdkManager.parsePropertyFile(propFile, null /* log */); if (map != null) { - return new ProjectProperties(projectFolderOsPath, map, type); + return new ProjectProperties(projectFolder, map, type); } } } @@ -172,11 +185,10 @@ public final class ProjectProperties { * @return this object, for chaining. */ public synchronized 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 (mProjectFolder.exists()) { + IAbstractFile propFile = mProjectFolder.getFile(type.mFilename); + if (propFile.exists()) { + Map<String, String> map = SdkManager.parsePropertyFile(propFile, null /* log */); if (map != null) { for(Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); @@ -195,11 +207,23 @@ public final class ProjectProperties { * 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 + * @param type the type of property file to create */ 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); + IAbstractFolder folder = new FolderWrapper(projectFolderOsPath); + return create(folder, type); + } + + /** + * Creates a new project properties object, with no properties. + * <p/>The file is not created until {@link #save()} is called. + * @param projectFolder the project folder. + * @param type the type of property file to create + */ + public static ProjectProperties create(IAbstractFolder projectFolder, PropertyType type) { + // create and return a ProjectProperties with an empty map. + return new ProjectProperties(projectFolder, new HashMap<String, String>(), type); } /** @@ -249,11 +273,10 @@ public final class ProjectProperties { * Reloads the properties from the underlying file. */ public synchronized void reload() { - File projectFolder = new File(mProjectFolderOsPath); - if (projectFolder.isDirectory()) { - File defaultFile = new File(projectFolder, mType.mFilename); - if (defaultFile.isFile()) { - Map<String, String> map = SdkManager.parsePropertyFile(defaultFile, null /* log */); + if (mProjectFolder.exists()) { + IAbstractFile propFile = mProjectFolder.getFile(mType.mFilename); + if (propFile.exists()) { + Map<String, String> map = SdkManager.parsePropertyFile(propFile, null /* log */); if (map != null) { mProperties.clear(); mProperties.putAll(map); @@ -265,11 +288,12 @@ public final class ProjectProperties { /** * Saves the property file, using UTF-8 encoding. * @throws IOException + * @throws StreamException */ - public synchronized void save() throws IOException { - File toSave = new File(mProjectFolderOsPath, mType.mFilename); + public synchronized void save() throws IOException, StreamException { + IAbstractFile toSave = mProjectFolder.getFile(mType.mFilename); - OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(toSave), + OutputStreamWriter writer = new OutputStreamWriter(toSave.getOutputStream(), SdkConstants.INI_CHARSET); // write the header @@ -298,9 +322,9 @@ public final class ProjectProperties { * Use {@link #load(String, PropertyType)} or {@link #create(String, PropertyType)} * to instantiate. */ - private ProjectProperties(String projectFolderOsPath, Map<String, String> map, + private ProjectProperties(IAbstractFolder projectFolder, Map<String, String> map, PropertyType type) { - mProjectFolderOsPath = projectFolderOsPath; + mProjectFolder = projectFolder; mProperties = map; mType = type; } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/FileWrapper.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/FileWrapper.java index 13dea12..9a0a4a6 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/FileWrapper.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/FileWrapper.java @@ -23,6 +23,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; /** @@ -115,6 +116,17 @@ public class FileWrapper extends File implements IAbstractFile { } } + public OutputStream getOutputStream() throws StreamException { + try { + return new FileOutputStream(this); + } catch (FileNotFoundException e) { + throw new StreamException(e); + } + } + + public PreferredWriteMode getPreferredWriteMode() { + return PreferredWriteMode.OUTPUTSTREAM; + } public String getOsLocation() { return getAbsolutePath(); @@ -124,4 +136,12 @@ public class FileWrapper extends File implements IAbstractFile { public boolean exists() { return isFile(); } + + public IAbstractFolder getParentFolder() { + String p = this.getParent(); + if (p == null) { + return null; + } + return new FolderWrapper(p); + } } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/FolderWrapper.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/FolderWrapper.java index 97cfad2..d009b7f 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/FolderWrapper.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/FolderWrapper.java @@ -18,8 +18,8 @@ package com.android.sdklib.io; import java.io.File; -import java.io.FilenameFilter; import java.net.URI; +import java.util.ArrayList; /** * An implementation of {@link IAbstractFolder} extending {@link File}. @@ -100,7 +100,7 @@ public class FolderWrapper extends File implements IAbstractFolder { public boolean hasFile(final String name) { String[] match = list(new FilenameFilter() { - public boolean accept(File dir, String filename) { + public boolean accept(IAbstractFolder dir, String filename) { return name.equals(filename); } }); @@ -112,8 +112,41 @@ public class FolderWrapper extends File implements IAbstractFolder { return new FileWrapper(this, name); } + public IAbstractFolder getFolder(String name) { + return new FolderWrapper(this, name); + } + + public IAbstractFolder getParentFolder() { + String p = this.getParent(); + if (p == null) { + return null; + } + return new FolderWrapper(p); + } + + public String getOsLocation() { + return getAbsolutePath(); + } + @Override public boolean exists() { return isDirectory(); } + + public String[] list(FilenameFilter filter) { + File[] files = listFiles(); + if (files.length > 0) { + ArrayList<String> list = new ArrayList<String>(); + + for (File file : files) { + if (filter.accept(this, file.getName())) { + list.add(file.getName()); + } + } + + return list.toArray(new String[list.size()]); + } + + return new String[0]; + } } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractFile.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractFile.java index a1fe667..2ff1fc8 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractFile.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractFile.java @@ -17,11 +17,15 @@ package com.android.sdklib.io; import java.io.InputStream; +import java.io.OutputStream; /** * A file. */ public interface IAbstractFile extends IAbstractResource { + public static enum PreferredWriteMode { + INPUTSTREAM, OUTPUTSTREAM; + } /** * Returns an {@link InputStream} object on the file content. @@ -37,7 +41,13 @@ public interface IAbstractFile extends IAbstractResource { void setContents(InputStream source) throws StreamException; /** - * Returns the OS path of the file location. + * Returns an {@link OutputStream} to write into the file. + * @throws StreamException + */ + OutputStream getOutputStream() throws StreamException; + + /** + * Returns the preferred mode to write into the file. */ - String getOsLocation(); + PreferredWriteMode getPreferredWriteMode(); } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractFolder.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractFolder.java index 80a6f84..bfbc86d 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractFolder.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractFolder.java @@ -16,11 +16,25 @@ package com.android.sdklib.io; - /** * A folder. */ public interface IAbstractFolder extends IAbstractResource { + /** + * Instances of classes that implement this interface are used to + * filter filenames. + */ + public interface FilenameFilter { + /** + * Tests if a specified file should be included in a file list. + * + * @param dir the directory in which the file was found. + * @param name the name of the file. + * @return <code>true</code> if and only if the name should be + * included in the file list; <code>false</code> otherwise. + */ + boolean accept(IAbstractFolder dir, String name); + } /** * Returns true if the receiver contains a file with a given name @@ -37,7 +51,16 @@ public interface IAbstractFolder extends IAbstractResource { IAbstractFile getFile(String name); /** + * returns an {@link IAbstractFolder} representing a child of the current folder with the + * given name. The folder may not actually exist. + * @param name the name of the folder. + */ + IAbstractFolder getFolder(String name); + + /** * returns a list of existing members in this folder. */ IAbstractResource[] listMembers(); + + String[] list(FilenameFilter filter); } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractResource.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractResource.java index ccc4988..0ccb107 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractResource.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/io/IAbstractResource.java @@ -29,7 +29,17 @@ public interface IAbstractResource { String getName(); /** + * Returns the OS path of the folder location. + */ + String getOsLocation(); + + /** * Returns whether the resource actually exists. */ boolean exists(); + + /** + * Returns the parent folder or null if there is no parent. + */ + IAbstractFolder getParentFolder(); } |