Home | History | Annotate | Download | only in export
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.sdklib.internal.export;
     18 
     19 import com.android.sdklib.SdkConstants;
     20 import com.android.sdklib.io.FileWrapper;
     21 import com.android.sdklib.io.IAbstractFile;
     22 import com.android.sdklib.io.StreamException;
     23 import com.android.sdklib.xml.AndroidManifestParser;
     24 import com.android.sdklib.xml.ManifestData;
     25 import com.android.sdklib.xml.ManifestData.SupportsScreens;
     26 
     27 import org.xml.sax.SAXException;
     28 
     29 import java.io.BufferedReader;
     30 import java.io.File;
     31 import java.io.FileInputStream;
     32 import java.io.FileOutputStream;
     33 import java.io.IOException;
     34 import java.io.InputStreamReader;
     35 import java.io.OutputStreamWriter;
     36 import java.io.PrintStream;
     37 import java.util.ArrayList;
     38 import java.util.Calendar;
     39 import java.util.Collections;
     40 import java.util.Formatter;
     41 import java.util.HashMap;
     42 import java.util.List;
     43 import java.util.Map;
     44 
     45 import javax.xml.parsers.ParserConfigurationException;
     46 
     47 /**
     48  * Helper to export multiple APKs from 1 or or more projects.
     49  * <strong>This class is not meant to be accessed from multiple threads</strong>
     50  */
     51 public class MultiApkExportHelper {
     52 
     53     private final static String PROP_VERSIONCODE = "versionCode";
     54     private final static String PROP_PACKAGE = "package";
     55 
     56     private final String mExportProjectRoot;
     57     private final String mAppPackage;
     58     private final int mVersionCode;
     59     private final Target mTarget;
     60 
     61     private ArrayList<ProjectConfig> mProjectList;
     62     private ArrayList<ApkData> mApkDataList;
     63 
     64     final static int MAX_MINOR = 100;
     65     final static int MAX_BUILDINFO = 100;
     66     final static int OFFSET_BUILD_INFO = MAX_MINOR;
     67     final static int OFFSET_VERSION_CODE = OFFSET_BUILD_INFO * MAX_BUILDINFO;
     68 
     69     private final static String FILE_CONFIG = "projects.config";
     70     private final static String FILE_MINOR_CODE = "minor.codes";
     71     private final static String FOLDER_LOG = "logs";
     72     private final PrintStream mStdio;
     73 
     74     public static final class ExportException extends Exception {
     75         private static final long serialVersionUID = 1L;
     76 
     77         public ExportException(String message) {
     78             super(message);
     79         }
     80 
     81         public ExportException(String format, Object... args) {
     82             super(String.format(format, args));
     83         }
     84 
     85         public ExportException(Throwable cause, String format, Object... args) {
     86             super(String.format(format, args), cause);
     87         }
     88 
     89         public ExportException(String message, Throwable cause) {
     90             super(message, cause);
     91         }
     92     }
     93 
     94     public static enum Target {
     95         RELEASE("release"), CLEAN("clean");
     96 
     97         private final String mName;
     98 
     99         Target(String name) {
    100             mName = name;
    101         }
    102 
    103         public String getTarget() {
    104             return mName;
    105         }
    106 
    107         public static Target getTarget(String value) {
    108             for (Target t : values()) {
    109                 if (t.mName.equals(value)) {
    110                     return t;
    111                 }
    112 
    113             }
    114 
    115             return null;
    116         }
    117     }
    118 
    119     public MultiApkExportHelper(String exportProjectRoot, String appPackage,
    120             int versionCode, Target target, PrintStream stdio) {
    121         mExportProjectRoot = exportProjectRoot;
    122         mAppPackage = appPackage;
    123         mVersionCode = versionCode;
    124         mTarget = target;
    125         mStdio = stdio;
    126     }
    127 
    128     public List<ApkData> getApkData(String projectList) throws ExportException {
    129         if (mTarget != Target.RELEASE) {
    130             throw new IllegalArgumentException("getApkData must only be called for Target.RELEASE");
    131         }
    132 
    133         // get the list of apk to export and their configuration.
    134         List<ProjectConfig> projects = getProjects(projectList);
    135 
    136         // look to see if there's a config file from a previous export
    137         File configProp = new File(mExportProjectRoot, FILE_CONFIG);
    138         if (configProp.isFile()) {
    139             compareProjectsToConfigFile(projects, configProp);
    140         }
    141 
    142         // look to see if there's a minor properties file
    143         File minorCodeProp = new File(mExportProjectRoot, FILE_MINOR_CODE);
    144         Map<Integer, Integer> minorCodeMap = null;
    145         if (minorCodeProp.isFile()) {
    146             minorCodeMap = getMinorCodeMap(minorCodeProp);
    147         }
    148 
    149         // get the apk from the projects.
    150         return getApkData(projects, minorCodeMap);
    151     }
    152 
    153     /**
    154      * Returns the list of projects defined by the <var>projectList</var> string.
    155      * The projects are checked to be valid Android project and to represent a valid set
    156      * of projects for multi-apk export.
    157      * If a project does not exist or is not valid, the method will throw a {@link BuildException}.
    158      * The string must be a list of paths, relative to the export project path (given to
    159      * {@link #MultiApkExportHelper(String, String, int, Target)}), separated by the colon (':')
    160      * character. The path separator is expected to be forward-slash ('/') on all platforms.
    161      * @param projects the string containing all the relative paths to the projects. This is
    162      * usually read from export.properties.
    163      * @throws ExportException
    164      */
    165     public List<ProjectConfig> getProjects(String projectList) throws ExportException {
    166         String[] paths = projectList.split("\\:");
    167 
    168         mProjectList = new ArrayList<ProjectConfig>();
    169 
    170         for (String path : paths) {
    171             path = path.replaceAll("\\/", File.separator);
    172             processProject(path, mProjectList);
    173         }
    174 
    175         return mProjectList;
    176     }
    177 
    178     /**
    179      * Writes post-export logs and other files.
    180      * @throws ExportException if writing the files failed.
    181      */
    182     public void writeLogs() throws ExportException {
    183         writeConfigProperties();
    184         writeMinorVersionProperties();
    185         writeApkLog();
    186     }
    187 
    188     private void writeConfigProperties() throws ExportException {
    189         OutputStreamWriter writer = null;
    190         try {
    191             writer = new OutputStreamWriter(
    192                     new FileOutputStream(new File(mExportProjectRoot, FILE_CONFIG)));
    193 
    194             writer.append("# PROJECT CONFIG -- DO NOT DELETE.\n");
    195             writeValue(writer, PROP_VERSIONCODE, mVersionCode);
    196 
    197             for (ProjectConfig project : mProjectList) {
    198                 writeValue(writer,project.getRelativePath(),
    199                         project.getConfigString(false /*onlyManifestData*/));
    200             }
    201 
    202             writer.flush();
    203         } catch (Exception e) {
    204             throw new ExportException("Failed to write config log", e);
    205         } finally {
    206             try {
    207                 if (writer != null) {
    208                     writer.close();
    209                 }
    210             } catch (IOException e) {
    211                 throw new ExportException("Failed to write config log", e);
    212             }
    213         }
    214     }
    215 
    216     private void writeMinorVersionProperties() throws ExportException {
    217         OutputStreamWriter writer = null;
    218         try {
    219             writer = new OutputStreamWriter(
    220                     new FileOutputStream(new File(mExportProjectRoot, FILE_MINOR_CODE)));
    221 
    222             writer.append(
    223                     "# Minor version codes.\n" +
    224                     "# To create update to select APKs without updating the main versionCode\n" +
    225                     "# edit this file and manually increase the minor version for the select\n" +
    226                     "# build info.\n" +
    227                     "# Format of the file is <buildinfo>:<minor>\n");
    228             writeValue(writer, PROP_VERSIONCODE, mVersionCode);
    229 
    230             for (ApkData apk : mApkDataList) {
    231                 writeValue(writer, Integer.toString(apk.getBuildInfo()), apk.getMinorCode());
    232             }
    233 
    234             writer.flush();
    235         } catch (Exception e) {
    236             throw new ExportException("Failed to write minor log", e);
    237         } finally {
    238             try {
    239                 if (writer != null) {
    240                     writer.close();
    241                 }
    242             } catch (IOException e) {
    243                 throw new ExportException("Failed to write minor log", e);
    244             }
    245         }
    246     }
    247 
    248     private void writeApkLog() throws ExportException {
    249         OutputStreamWriter writer = null;
    250         try {
    251             File logFolder = new File(mExportProjectRoot, FOLDER_LOG);
    252             if (logFolder.isFile()) {
    253                 throw new ExportException("Cannot create folder '%1$s', file is in the way!",
    254                         FOLDER_LOG);
    255             } else if (logFolder.exists() == false) {
    256                 logFolder.mkdir();
    257             }
    258 
    259             Formatter formatter = new Formatter();
    260             formatter.format("%1$s.%2$d-%3$tY%3$tm%3$td-%3$tH%3$tM.log",
    261                     mAppPackage, mVersionCode,
    262                     Calendar.getInstance().getTime());
    263 
    264             writer = new OutputStreamWriter(
    265                     new FileOutputStream(new File(logFolder, formatter.toString())));
    266 
    267             writer.append("# Multi-APK BUILD LOG.\n");
    268             writeValue(writer, PROP_PACKAGE, mAppPackage);
    269             writeValue(writer, PROP_VERSIONCODE, mVersionCode);
    270 
    271             for (ApkData apk : mApkDataList) {
    272                 // if there are soft variant, do not display the main log line, as it's not actually
    273                 // exported.
    274                 Map<String, String> softVariants = apk.getSoftVariantMap();
    275                 if (softVariants.size() > 0) {
    276                     for (String softVariant : softVariants.keySet()) {
    277                         writer.append(apk.getLogLine(softVariant));
    278                         writer.append('\n');
    279                     }
    280                 } else {
    281                     writer.append(apk.getLogLine(null));
    282                     writer.append('\n');
    283                 }
    284             }
    285 
    286             writer.flush();
    287         } catch (Exception e) {
    288             throw new ExportException("Failed to write build log", e);
    289         } finally {
    290             try {
    291                 if (writer != null) {
    292                     writer.close();
    293                 }
    294             } catch (IOException e) {
    295                 throw new ExportException("Failed to write build log", e);
    296             }
    297         }
    298     }
    299 
    300     private void writeValue(OutputStreamWriter writer, String name, String value)
    301             throws IOException {
    302         writer.append(name).append(':').append(value).append('\n');
    303     }
    304 
    305     private void writeValue(OutputStreamWriter writer, String name, int value) throws IOException {
    306         writeValue(writer, name, Integer.toString(value));
    307     }
    308 
    309     private List<ApkData> getApkData(List<ProjectConfig> projects,
    310             Map<Integer, Integer> minorCodes) {
    311         mApkDataList = new ArrayList<ApkData>();
    312 
    313         // get all the apkdata from all the projects
    314         for (ProjectConfig config : projects) {
    315             mApkDataList.addAll(config.getApkDataList());
    316         }
    317 
    318         // sort the projects and assign buildInfo
    319         Collections.sort(mApkDataList);
    320         int buildInfo = 0;
    321         for (ApkData data : mApkDataList) {
    322             data.setBuildInfo(buildInfo);
    323             if (minorCodes != null) {
    324                 Integer minorCode = minorCodes.get(buildInfo);
    325                 if (minorCode != null) {
    326                     data.setMinorCode(minorCode);
    327                 }
    328             }
    329 
    330             buildInfo++;
    331         }
    332 
    333         return mApkDataList;
    334     }
    335 
    336     /**
    337      * Checks a project for inclusion in the list of exported APK.
    338      * <p/>This performs a check on the manifest, as well as gathers more information about
    339      * mutli-apk from the project's default.properties file.
    340      * If the manifest is correct, a list of apk to export is created and returned.
    341      *
    342      * @param projectFolder the folder of the project to check
    343      * @param projects the list of project to file with the project if it passes validation.
    344      * @throws ExportException in case of error.
    345      */
    346     private void processProject(String relativePath,
    347             ArrayList<ProjectConfig> projects) throws ExportException {
    348 
    349         // resolve the relative path
    350         File projectFolder;
    351         try {
    352             File path = new File(mExportProjectRoot, relativePath);
    353 
    354             projectFolder = path.getCanonicalFile();
    355 
    356             // project folder must exist and be a directory
    357             if (projectFolder.isDirectory() == false) {
    358                 throw new ExportException(
    359                         "Project folder '%1$s' is not a valid directory.",
    360                         projectFolder.getAbsolutePath());
    361             }
    362         } catch (IOException e) {
    363             throw new ExportException(
    364                     e, "Failed to resolve path %1$s", relativePath);
    365         }
    366 
    367         try {
    368             // Check AndroidManifest.xml is present
    369             IAbstractFile androidManifest = new FileWrapper(projectFolder,
    370                     SdkConstants.FN_ANDROID_MANIFEST_XML);
    371 
    372             if (androidManifest.exists() == false) {
    373                 throw new ExportException(String.format(
    374                         "%1$s is not a valid project (%2$s not found).",
    375                         relativePath, androidManifest.getOsLocation()));
    376             }
    377 
    378             // output the relative path resolution.
    379             mStdio.println(String.format("%1$s => %2$s", relativePath,
    380                     projectFolder.getAbsolutePath()));
    381 
    382             // parse the manifest of the project.
    383             ManifestData manifestData = AndroidManifestParser.parse(androidManifest);
    384 
    385             // validate the application package name
    386             String manifestPackage = manifestData.getPackage();
    387             if (mAppPackage.equals(manifestPackage) == false) {
    388                 throw new ExportException(
    389                         "%1$s package value is not valid. Found '%2$s', expected '%3$s'.",
    390                         androidManifest.getOsLocation(), manifestPackage, mAppPackage);
    391             }
    392 
    393             // validate that the manifest has no versionCode set.
    394             if (manifestData.getVersionCode() != null) {
    395                 throw new ExportException(
    396                         "%1$s is not valid: versionCode must not be set for multi-apk export.",
    397                         androidManifest.getOsLocation());
    398             }
    399 
    400             // validate that the minSdkVersion is not a codename
    401             int minSdkVersion = manifestData.getMinSdkVersion();
    402             if (minSdkVersion == ManifestData.MIN_SDK_CODENAME) {
    403                 throw new ExportException(
    404                         "Codename in minSdkVersion is not supported by multi-apk export.");
    405             }
    406 
    407             // compare to other projects already processed to make sure that they are not
    408             // identical.
    409             for (ProjectConfig otherProject : projects) {
    410                 // Multiple apk export support difference in:
    411                 // - min SDK Version
    412                 // - Screen version
    413                 // - GL version
    414                 // - ABI (not managed at the Manifest level).
    415                 // if those values are the same between 2 manifest, then it's an error.
    416 
    417 
    418                 // first the minSdkVersion.
    419                 if (minSdkVersion == otherProject.getMinSdkVersion()) {
    420                     // if it's the same compare the rest.
    421                     SupportsScreens currentSS = manifestData.getSupportsScreensValues();
    422                     SupportsScreens previousSS = otherProject.getSupportsScreens();
    423                     boolean sameSupportsScreens = currentSS.hasSameScreenSupportAs(previousSS);
    424 
    425                     // if it's the same, then it's an error. Can't export 2 projects that have the
    426                     // same approved (for multi-apk export) hard-properties.
    427                     if (manifestData.getGlEsVersion() == otherProject.getGlEsVersion() &&
    428                             sameSupportsScreens) {
    429 
    430                         throw new ExportException(
    431                                 "Android manifests must differ in at least one of the following values:\n" +
    432                                 "- minSdkVersion\n" +
    433                                 "- SupportsScreen (screen sizes only)\n" +
    434                                 "- GL ES version.\n" +
    435                                 "%1$s and %2$s are considered identical for multi-apk export.",
    436                                 relativePath,
    437                                 otherProject.getRelativePath());
    438                     }
    439 
    440                     // At this point, either supports-screens or GL are different.
    441                     // Because supports-screens is the highest priority properties to be
    442                     // (potentially) different, we must do some extra checks on it.
    443                     // It must either be the same in both projects (difference is only on GL value),
    444                     // or follow theses rules:
    445                     // - Property in each projects must be strictly different, ie both projects
    446                     //   cannot support the same screen size(s).
    447                     // - Property in each projects cannot overlap, ie a projects cannot support
    448                     //   both a lower and a higher screen size than the other project.
    449                     //   (ie APK1 supports small/large and APK2 supports normal).
    450                     if (sameSupportsScreens == false) {
    451                         if (currentSS.hasStrictlyDifferentScreenSupportAs(previousSS) == false) {
    452                             throw new ExportException(
    453                                     "APK differentiation by Supports-Screens cannot support different APKs supporting the same screen size.\n" +
    454                                     "%1$s supports %2$s\n" +
    455                                     "%3$s supports %4$s\n",
    456                                     relativePath, currentSS.toString(),
    457                                     otherProject.getRelativePath(), previousSS.toString());
    458                         }
    459 
    460                         if (currentSS.overlapWith(previousSS)) {
    461                             throw new ExportException(
    462                                     "Unable to compute APK priority due to incompatible difference in Supports-Screens values.\n" +
    463                                     "%1$s supports %2$s\n" +
    464                                     "%3$s supports %4$s\n",
    465                                     relativePath, currentSS.toString(),
    466                                     otherProject.getRelativePath(), previousSS.toString());
    467                         }
    468                     }
    469                 }
    470             }
    471 
    472             // project passes first validation. Attempt to create a ProjectConfig object.
    473 
    474             ProjectConfig config = ProjectConfig.create(projectFolder, relativePath, manifestData);
    475             projects.add(config);
    476         } catch (SAXException e) {
    477             throw new ExportException(e, "Failed to validate %1$s", relativePath);
    478         } catch (IOException e) {
    479             throw new ExportException(e, "Failed to validate %1$s", relativePath);
    480         } catch (StreamException e) {
    481             throw new ExportException(e, "Failed to validate %1$s", relativePath);
    482         } catch (ParserConfigurationException e) {
    483             throw new ExportException(e, "Failed to validate %1$s", relativePath);
    484         }
    485     }
    486 
    487     /**
    488      * Checks an existing list of {@link ProjectConfig} versus a config file.
    489      * @param projects the list of projects to check
    490      * @param configProp the config file (must have been generated from a previous export)
    491      * @return true if the projects and config file match
    492      * @throws ExportException in case of error
    493      */
    494     private void compareProjectsToConfigFile(List<ProjectConfig> projects, File configProp)
    495             throws ExportException {
    496         InputStreamReader reader = null;
    497         BufferedReader bufferedReader = null;
    498         try {
    499             reader = new InputStreamReader(new FileInputStream(configProp));
    500             bufferedReader = new BufferedReader(reader);
    501             String line;
    502 
    503             // List of the ProjectConfig that need to be checked. This is to detect
    504             // new Projects added to the setup.
    505             // removed projects are detected when an entry in the config file doesn't match
    506             // any ProjectConfig in the list.
    507             ArrayList<ProjectConfig> projectsToCheck = new ArrayList<ProjectConfig>();
    508             projectsToCheck.addAll(projects);
    509 
    510             // store the project that doesn't match.
    511             ProjectConfig badMatch = null;
    512             String errorMsg = null;
    513 
    514             // recorded whether we checked the version code. this is for when we compare
    515             // a project config
    516             boolean checkedVersion = false;
    517 
    518             int lineNumber = 0;
    519             while ((line = bufferedReader.readLine()) != null) {
    520                 lineNumber++;
    521                 line = line.trim();
    522                 if (line.length() == 0 || line.startsWith("#")) {
    523                     continue;
    524                 }
    525 
    526                 // read the name of the property
    527                 int colonPos = line.indexOf(':');
    528                 if (colonPos == -1) {
    529                     // looks like there's an invalid line!
    530                     throw new ExportException(
    531                             "Failed to read existing build log. Line %d is not a property line.",
    532                             lineNumber);
    533                 }
    534 
    535                 String name = line.substring(0, colonPos);
    536                 String value = line.substring(colonPos + 1);
    537 
    538                 if (PROP_VERSIONCODE.equals(name)) {
    539                     try {
    540                         int versionCode = Integer.parseInt(value);
    541                         if (versionCode < mVersionCode) {
    542                             // this means this config file is obsolete and we can ignore it.
    543                             return;
    544                         } else if (versionCode > mVersionCode) {
    545                             // we're exporting at a lower versionCode level than the config file?
    546                             throw new ExportException(
    547                                     "Incompatible versionCode: Exporting at versionCode %1$d but %2$s file indicate previous export with versionCode %3$d.",
    548                                     mVersionCode, FILE_CONFIG, versionCode);
    549                         } else if (badMatch != null) {
    550                             // looks like versionCode is a match, but a project
    551                             // isn't compatible.
    552                             break;
    553                         } else {
    554                             // record that we did check the versionCode
    555                             checkedVersion = true;
    556                         }
    557                     } catch (NumberFormatException e) {
    558                         throw new ExportException(
    559                                 "Failed to read integer property %1$s at line %2$d.",
    560                                 PROP_VERSIONCODE, lineNumber);
    561                     }
    562                 } else {
    563                     // looks like this is (or should be) a project line.
    564                     // name of the property is the relative path.
    565                     // look for a matching project.
    566                     ProjectConfig found = null;
    567                     for (int i = 0 ; i < projectsToCheck.size() ; i++) {
    568                         ProjectConfig p = projectsToCheck.get(i);
    569                         if (p.getRelativePath().equals(name)) {
    570                             found = p;
    571                             projectsToCheck.remove(i);
    572                             break;
    573                         }
    574                     }
    575 
    576                     if (found == null) {
    577                         // deleted project!
    578                         throw new ExportException(
    579                                 "Project %1$s has been removed from the list of projects to export.\n" +
    580                                 "Any change in the multi-apk configuration requires an increment of the versionCode in export.properties.",
    581                                 name);
    582                     } else {
    583                         // make a map of properties
    584                         HashMap<String, String> map = new HashMap<String, String>();
    585                         String[] properties = value.split(";");
    586                         for (String prop : properties) {
    587                             int equalPos = prop.indexOf('=');
    588                             map.put(prop.substring(0, equalPos), prop.substring(equalPos + 1));
    589                         }
    590 
    591                         errorMsg = found.compareToProperties(map);
    592                         if (errorMsg != null) {
    593                             // bad project config, record the project
    594                             badMatch = found;
    595 
    596                             // if we've already checked that the versionCode didn't already change
    597                             // we stop right away.
    598                             if (checkedVersion) {
    599                                 break;
    600                             }
    601                         }
    602                     }
    603 
    604                 }
    605 
    606             }
    607 
    608             if (badMatch != null) {
    609                 throw new ExportException(
    610                         "Config for project %1$s has changed from previous export with versionCode %2$d:\n" +
    611                         "\t%3$s\n" +
    612                         "Any change in the multi-apk configuration requires an increment of the versionCode in export.properties.",
    613                         badMatch.getRelativePath(), mVersionCode, errorMsg);
    614             } else if (projectsToCheck.size() > 0) {
    615                 throw new ExportException(
    616                         "Project %1$s was not part of the previous export with versionCode %2$d.\n" +
    617                         "Any change in the multi-apk configuration requires an increment of the versionCode in export.properties.",
    618                         projectsToCheck.get(0).getRelativePath(), mVersionCode);
    619             }
    620 
    621         } catch (IOException e) {
    622             throw new ExportException(e, "Failed to read existing config log: %s", FILE_CONFIG);
    623         } finally {
    624             try {
    625                 if (reader != null) {
    626                     reader.close();
    627                 }
    628             } catch (IOException e) {
    629                 throw new ExportException(e, "Failed to read existing config log: %s", FILE_CONFIG);
    630             }
    631         }
    632     }
    633 
    634     private Map<Integer, Integer> getMinorCodeMap(File minorProp) throws ExportException {
    635         InputStreamReader reader = null;
    636         BufferedReader bufferedReader = null;
    637         try {
    638             reader = new InputStreamReader(new FileInputStream(minorProp));
    639             bufferedReader = new BufferedReader(reader);
    640             String line;
    641 
    642             boolean foundVersionCode = false;
    643             Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    644 
    645             int lineNumber = 0;
    646             while ((line = bufferedReader.readLine()) != null) {
    647                 lineNumber++;
    648                 line = line.trim();
    649                 if (line.length() == 0 || line.startsWith("#")) {
    650                     continue;
    651                 }
    652 
    653                 // read the name of the property
    654                 int colonPos = line.indexOf(':');
    655                 if (colonPos == -1) {
    656                     // looks like there's an invalid line!
    657                     throw new ExportException(
    658                             "Failed to read existing build log. Line %d is not a property line.",
    659                             lineNumber);
    660                 }
    661 
    662                 String name = line.substring(0, colonPos);
    663                 String value = line.substring(colonPos + 1);
    664 
    665                 if (PROP_VERSIONCODE.equals(name)) {
    666                     try {
    667                         int versionCode = Integer.parseInt(value);
    668                         if (versionCode < mVersionCode) {
    669                             // this means this minor file is obsolete and we can ignore it.
    670                             return null;
    671                         } else if (versionCode > mVersionCode) {
    672                             // we're exporting at a lower versionCode level than the minor file?
    673                             throw new ExportException(
    674                                     "Incompatible versionCode: Exporting at versionCode %1$d but %2$s file indicate previous export with versionCode %3$d.",
    675                                     mVersionCode, FILE_MINOR_CODE, versionCode);
    676                         }
    677                         foundVersionCode = true;
    678                     } catch (NumberFormatException e) {
    679                         throw new ExportException(
    680                                 "Failed to read integer property %1$s at line %2$d.",
    681                                 PROP_VERSIONCODE, lineNumber);
    682                     }
    683                 } else {
    684                     try {
    685                         map.put(Integer.valueOf(name), Integer.valueOf(value));
    686                     } catch (NumberFormatException e) {
    687                         throw new ExportException(
    688                                 "Failed to read buildInfo property '%1$s' at line %2$d.",
    689                                 line, lineNumber);
    690                     }
    691                 }
    692             }
    693 
    694             // if there was no versionCode found, we can't garantee that the minor version
    695             // found are for this versionCode
    696             if (foundVersionCode == false) {
    697                 throw new ExportException(
    698                         "%1$s property missing from file %2$s.", PROP_VERSIONCODE, FILE_MINOR_CODE);
    699             }
    700 
    701             return map;
    702         } catch (IOException e) {
    703             throw new ExportException(e, "Failed to read existing minor log: %s", FILE_MINOR_CODE);
    704         } finally {
    705             try {
    706                 if (reader != null) {
    707                     reader.close();
    708                 }
    709             } catch (IOException e) {
    710                 throw new ExportException(e, "Failed to read existing minor log: %s",
    711                         FILE_MINOR_CODE);
    712             }
    713         }
    714     }
    715 }
    716