manifest-merger

16:39:05.532 INFO  jd.cli.Main - Decompiling manifest-merger.jar
package com.android.manifmerger;

import com.android.sdklib.util.CommandLineParser;
import com.android.sdklib.util.CommandLineParser.Mode;
import com.android.utils.ILogger;
import java.util.List;

class ArgvParser
  extends CommandLineParser
{
  public static final String VERB_MERGE = "merge";
  public static final String KEY_OUT = "out";
  public static final String KEY_MAIN = "main";
  public static final String KEY_LIBS = "libs";
  private static final String[][] ACTIONS = { { "merge", "", "Merge two or more manifests." } };
  
  public ArgvParser(ILogger logger)
  {
    super(logger, ACTIONS);
    
    define(CommandLineParser.Mode.STRING, true, "merge", "", "o", "out", "Output path (where to write the merged manifest). Use - for stdout.", null);
    
    define(CommandLineParser.Mode.STRING, true, "merge", "", "1", "main", "Path of the main manifest (what to merge *into*)", null);
    
    define(CommandLineParser.Mode.STRING_ARRAY, true, "merge", "", "2", "libs", "Paths of library manifests to be merged into the main one.", null);
  }
  
  public boolean acceptLackOfVerb()
  {
    return true;
  }
  
  public String getParamOut()
  {
    return (String)getValue(null, null, "out");
  }
  
  public String getParamMain()
  {
    return (String)getValue(null, null, "main");
  }
  
  public String[] getParamLibs()
  {
    Object v = getValue(null, null, "libs");
    if ((v instanceof List))
    {
      List<?> a = (List)v;
      return (String[])a.toArray(new String[a.size()]);
    }
    return null;
  }
}

/* Location:
 * Qualified Name:     com.android.manifmerger.ArgvParser
 * Java Class Version: 6 (50.0)
 * JD-Core Version:    0.7.1
 */
package com.android.manifmerger;

import com.android.annotations.NonNull;

public abstract interface ICallback
{
  public static final int UNKNOWN_CODENAME = 0;
  
  public abstract int queryCodenameApiLevel(@NonNull String paramString);
}

/* Location:
 * Qualified Name:     com.android.manifmerger.ICallback
 * Java Class Version: 6 (50.0)
 * JD-Core Version:    0.7.1
 */
package com.android.manifmerger;

import com.android.annotations.Nullable;

public class IMergerLog$FileAndLine
{
  private final String mFilePath;
  private final int mLine;
  
  public IMergerLog$FileAndLine(@Nullable String filePath, int line)
  {
    mFilePath = filePath;
    mLine = line;
  }
  
  @Nullable
  public String getFileName()
  {
    return mFilePath;
  }
  
  public int getLine()
  {
    return mLine;
  }
  
  public String toString()
  {
    String name = mFilePath;
    if ("@main".equals(name)) {
      name = "main manifest";
    } else if ("@library".equals(name)) {
      name = "library";
    } else if (name == null) {
      name = "(Unknown)";
    }
    if (mLine <= 0) {
      return name;
    }
    return name + ':' + mLine;
  }
}

/* Location:
 * Qualified Name:     com.android.manifmerger.IMergerLog.FileAndLine
 * Java Class Version: 6 (50.0)
 * JD-Core Version:    0.7.1
 */
package com.android.manifmerger;

public enum IMergerLog$Severity
{
  INFO,  WARNING,  ERROR;
  
  private IMergerLog$Severity() {}
}

/* Location:
 * Qualified Name:     com.android.manifmerger.IMergerLog.Severity
 * Java Class Version: 6 (50.0)
 * JD-Core Version:    0.7.1
 */
package com.android.manifmerger;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;

public abstract interface IMergerLog
{
  public static final String MAIN_MANIFEST = "@main";
  public static final String LIBRARY = "@library";
  
  public abstract void error(@NonNull Severity paramSeverity, @NonNull FileAndLine paramFileAndLine, @NonNull String paramString, Object... paramVarArgs);
  
  public abstract void conflict(@NonNull Severity paramSeverity, @NonNull FileAndLine paramFileAndLine1, @NonNull FileAndLine paramFileAndLine2, @NonNull String paramString, Object... paramVarArgs);
  
  public static enum Severity
  {
    INFO,  WARNING,  ERROR;
    
    private Severity() {}
  }
  
  public static class FileAndLine
  {
    private final String mFilePath;
    private final int mLine;
    
    public FileAndLine(@Nullable String filePath, int line)
    {
      mFilePath = filePath;
      mLine = line;
    }
    
    @Nullable
    public String getFileName()
    {
      return mFilePath;
    }
    
    public int getLine()
    {
      return mLine;
    }
    
    public String toString()
    {
      String name = mFilePath;
      if ("@main".equals(name)) {
        name = "main manifest";
      } else if ("@library".equals(name)) {
        name = "library";
      } else if (name == null) {
        name = "(Unknown)";
      }
      if (mLine <= 0) {
        return name;
      }
      return name + ':' + mLine;
    }
  }
}

/* Location:
 * Qualified Name:     com.android.manifmerger.IMergerLog
 * Java Class Version: 6 (50.0)
 * JD-Core Version:    0.7.1
 */
package com.android.manifmerger;

import com.android.utils.ILogger;
import com.android.utils.StdLogger;
import com.android.utils.StdLogger.Level;
import java.io.File;

public class Main
{
  private ILogger mSdkLog;
  private ArgvParser mArgvParser;
  
  public static void main(String[] args)
  {
    new Main().run(args);
  }
  
  private void run(String[] args)
  {
    createLogger();
    
    mArgvParser = new ArgvParser(mSdkLog);
    mArgvParser.parseArgs(args);
    
    ManifestMerger mm = new ManifestMerger(MergerLog.wrapSdkLog(mSdkLog), null);
    
    String[] libPaths = mArgvParser.getParamLibs();
    File[] libFiles = new File[libPaths.length];
    for (int n = libPaths.length - 1; n >= 0; n--) {
      libFiles[n] = new File(libPaths[n]);
    }
    boolean ok = mm.process(new File(mArgvParser.getParamOut()), new File(mArgvParser.getParamMain()), libFiles, null, null);
    
    System.exit(ok ? 0 : 1);
  }
  
  private void createLogger()
  {
    mSdkLog = new StdLogger(StdLogger.Level.VERBOSE);
  }
  
  public void setLogger(ILogger logger)
  {
    mSdkLog = logger;
  }
}

/* Location:
 * Qualified Name:     com.android.manifmerger.Main
 * Java Class Version: 6 (50.0)
 * JD-Core Version:    0.7.1
 */
package com.android.manifmerger;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
import com.android.xml.AndroidXPathFactory;
import java.io.File;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class ManifestMerger
{
  private final IMergerLog mLog;
  private final ICallback mCallback;
  private XPath mXPath;
  private Document mMainDoc;
  private boolean mExtractPackagePrefix;
  private boolean mInsertSourceMarkers;
  private static final String NS_URI = "http://schemas.android.com/apk/res/android";
  private static final String NS_PREFIX = "android";
  private static final String TOOLS_URI = "http://schemas.android.com/tools";
  private static final String MERGE_ATTR = "merge";
  private static final String MERGE_OVERRIDE = "override";
  private static final String MERGE_REMOVE = "remove";
  private static final String[] sClassAttributes = { "application/name", "application/backupAgent", "activity/name", "activity/parentActivityName", "activity-alias/name", "receiver/name", "service/name", "provider/name", "instrumentation/name" };
  
  public ManifestMerger(@NonNull IMergerLog log, @Nullable ICallback callback)
  {
    mLog = log;
    mCallback = callback;
  }
  
  public ManifestMerger setExtractPackagePrefix(boolean extract)
  {
    mExtractPackagePrefix = extract;
    return this;
  }
  
  public boolean process(File outputFile, File mainFile, File[] libraryFiles, Map<String, String> injectAttributes, String packageOverride)
  {
    Document mainDoc = MergerXmlUtils.parseDocument(mainFile, mLog, this);
    if (mainDoc == null)
    {
      mLog.error(IMergerLog.Severity.ERROR, new IMergerLog.FileAndLine(mainFile.getAbsolutePath(), 0), "Failed to read manifest file.", new Object[0]);
      
      return false;
    }
    boolean success = process(mainDoc, libraryFiles, injectAttributes, packageOverride);
    if (!MergerXmlUtils.printXmlFile(mainDoc, outputFile, mLog))
    {
      mLog.error(IMergerLog.Severity.ERROR, new IMergerLog.FileAndLine(outputFile.getAbsolutePath(), 0), "Failed to write manifest file.", new Object[0]);
      
      success = false;
    }
    return success;
  }
  
  public boolean process(Document mainDoc, File[] libraryFiles, Map<String, String> injectAttributes, String packageOverride)
  {
    boolean success = true;
    mMainDoc = mainDoc;
    MergerXmlUtils.decorateDocument(mainDoc, "@main");
    MergerXmlUtils.injectAttributes(mainDoc, injectAttributes, mLog);
    
    String prefix = XmlUtils.lookupNamespacePrefix(mainDoc, "http://schemas.android.com/apk/res/android");
    mXPath = AndroidXPathFactory.newXPath(prefix);
    
    expandFqcns(mainDoc);
    for (File libFile : libraryFiles)
    {
      Document libDoc = MergerXmlUtils.parseDocument(libFile, mLog, this);
      if ((libDoc == null) || (!mergeLibDoc(cleanupToolsAttributes(libDoc)))) {
        success = false;
      }
    }
    if (packageOverride != null) {
      MergerXmlUtils.injectAttributes(mainDoc, Collections.singletonMap("/manifest| package", packageOverride), mLog);
    }
    cleanupToolsAttributes(mainDoc);
    if (mExtractPackagePrefix) {
      extractFqcns(mainDoc);
    }
    if (mInsertSourceMarkers) {
      insertSourceMarkers(mainDoc);
    }
    mXPath = null;
    mMainDoc = null;
    return success;
  }
  
  public boolean process(@NonNull Document mainDoc, @NonNull Document... libraryDocs)
  {
    boolean success = true;
    mMainDoc = mainDoc;
    MergerXmlUtils.decorateDocument(mainDoc, "@main");
    
    String prefix = XmlUtils.lookupNamespacePrefix(mainDoc, "http://schemas.android.com/apk/res/android");
    mXPath = AndroidXPathFactory.newXPath(prefix);
    
    expandFqcns(mainDoc);
    for (Document libDoc : libraryDocs)
    {
      MergerXmlUtils.decorateDocument(libDoc, "@library");
      if (!mergeLibDoc(cleanupToolsAttributes(libDoc))) {
        success = false;
      }
    }
    cleanupToolsAttributes(mainDoc);
    if (mInsertSourceMarkers) {
      insertSourceMarkers(mainDoc);
    }
    mXPath = null;
    mMainDoc = null;
    return success;
  }
  
  private boolean mergeLibDoc(Document libDoc)
  {
    boolean err = false;
    
    expandFqcns(libDoc);
    
    err |= !checkApplication(libDoc);
    
    err |= !doNotMergeCheckEqual("/manifest/uses-configuration", libDoc);
    err |= !doNotMergeCheckEqual("/manifest/supports-screens", libDoc);
    err |= !doNotMergeCheckEqual("/manifest/compatible-screens", libDoc);
    err |= !doNotMergeCheckEqual("/manifest/supports-gl-texture", libDoc);
    
    boolean skipApplication = hasOverrideOrRemoveTag(findFirstElement(mMainDoc, "/manifest/application"));
    if (!skipApplication)
    {
      err |= !mergeNewOrEqual("/manifest/application/activity", "name", libDoc, true);
      
      err |= !mergeNewOrEqual("/manifest/application/activity-alias", "name", libDoc, true);
      
      err |= !mergeNewOrEqual("/manifest/application/service", "name", libDoc, true);
      
      err |= !mergeNewOrEqual("/manifest/application/receiver", "name", libDoc, true);
      
      err |= !mergeNewOrEqual("/manifest/application/provider", "name", libDoc, true);
    }
    err |= !mergeNewOrEqual("/manifest/permission", "name", libDoc, false);
    
    err |= !mergeNewOrEqual("/manifest/permission-group", "name", libDoc, false);
    
    err |= !mergeNewOrEqual("/manifest/permission-tree", "name", libDoc, false);
    
    err |= !mergeNewOrEqual("/manifest/uses-permission", "name", libDoc, false);
    if (!skipApplication)
    {
      err |= !mergeAdjustRequired("/manifest/application/uses-library", "name", "required", libDoc, null);
      
      err |= !mergeNewOrEqual("/manifest/application/meta-data", "name", libDoc, true);
    }
    err |= !mergeAdjustRequired("/manifest/uses-feature", "name", "required", libDoc, "glEsVersion");
    
    err |= !checkSdkVersion(libDoc);
    
    err |= !checkGlEsVersion(libDoc);
    
    return !err;
  }
  
  private void expandFqcns(Document doc)
  {
    String pkg = null;
    Element manifest = findFirstElement(doc, "/manifest");
    if (manifest != null) {
      pkg = manifest.getAttribute("package");
    }
    if ((pkg == null) || (pkg.length() == 0))
    {
      assert (manifest != null);
      mLog.error(IMergerLog.Severity.WARNING, xmlFileAndLine(manifest), "Missing 'package' attribute in manifest.", new Object[0]);
      
      return;
    }
    for (String elementAttr : sClassAttributes)
    {
      String[] names = elementAttr.split("/");
      if (names.length == 2)
      {
        String elemName = names[0];
        String attrName = names[1];
        NodeList elements = doc.getElementsByTagName(elemName);
        for (int i = 0; i < elements.getLength(); i++)
        {
          Node elem = elements.item(i);
          if ((elem instanceof Element))
          {
            Attr attr = ((Element)elem).getAttributeNodeNS("http://schemas.android.com/apk/res/android", attrName);
            if (attr != null)
            {
              String value = attr.getNodeValue();
              if ((value != null) && (value.length() > 0) && ((value.indexOf('.') == -1) || (value.charAt(0) == '.')))
              {
                if (value.charAt(0) == '.') {
                  value = pkg + value;
                } else {
                  value = pkg + '.' + value;
                }
                attr.setNodeValue(value);
              }
            }
          }
        }
      }
    }
  }
  
  private void extractFqcns(Document doc)
  {
    String pkg = null;
    Element manifest = findFirstElement(doc, "/manifest");
    if (manifest != null) {
      pkg = manifest.getAttribute("package");
    }
    if ((pkg == null) || (pkg.length() == 0)) {
      return;
    }
    int pkgLength = pkg.length();
    for (String elementAttr : sClassAttributes)
    {
      String[] names = elementAttr.split("/");
      if (names.length == 2)
      {
        String elemName = names[0];
        String attrName = names[1];
        NodeList elements = doc.getElementsByTagName(elemName);
        for (int i = 0; i < elements.getLength(); i++)
        {
          Node elem = elements.item(i);
          if ((elem instanceof Element))
          {
            Attr attr = ((Element)elem).getAttributeNodeNS("http://schemas.android.com/apk/res/android", attrName);
            if (attr != null)
            {
              String value = attr.getNodeValue();
              if ((value != null) && (value.length() > pkgLength) && (value.startsWith(pkg)) && (value.charAt(pkgLength) == '.'))
              {
                value = value.substring(pkgLength);
                attr.setNodeValue(value);
              }
            }
          }
        }
      }
    }
  }
  
  private boolean checkApplication(Document libDoc)
  {
    Element mainApp = findFirstElement(mMainDoc, "/manifest/application");
    Element libApp = findFirstElement(libDoc, "/manifest/application");
    if (libApp == null) {
      return true;
    }
    if (hasOverrideOrRemoveTag(mainApp)) {
      return true;
    }
    for (String attrName : new String[] { "name", "backupAgent" })
    {
      String libValue = getAttributeValue(libApp, attrName);
      if ((libValue != null) && (libValue.length() != 0))
      {
        String mainValue = mainApp == null ? "" : getAttributeValue(mainApp, attrName);
        if (!libValue.equals(mainValue))
        {
          assert (mainApp != null);
          mLog.conflict(IMergerLog.Severity.WARNING, xmlFileAndLine(mainApp), xmlFileAndLine(libApp), mainApp == null ? "Library has <application android:%1$s='%3$s'> but main manifest has no application element." : "Main manifest has <application android:%1$s='%2$s'> but library uses %1$s='%3$s'.", new Object[] { attrName, mainValue, libValue });
        }
      }
    }
    return true;
  }
  
  private boolean doNotMergeCheckEqual(String path, Document libDoc)
  {
    for (Element src : findElements(libDoc, path))
    {
      boolean found = false;
      for (Element dest : findElements(mMainDoc, path)) {
        if (!hasOverrideOrRemoveTag(dest)) {
          if (compareElements(dest, src, false, null, null))
          {
            found = true;
            break;
          }
        }
      }
      if (!found) {
        mLog.conflict(IMergerLog.Severity.WARNING, xmlFileAndLine(mMainDoc), xmlFileAndLine(src), "%1$s defined in library, missing from main manifest:\n%2$s", new Object[] { path, MergerXmlUtils.dump(src, false) });
      }
    }
    return true;
  }
  
  private boolean mergeNewOrEqual(String path, String keyAttr, Document libDoc, boolean warnDups)
  {
    int pos = path.lastIndexOf('/');
    assert (pos > 1);
    String parentPath = path.substring(0, pos);
    Element parent = findFirstElement(mMainDoc, parentPath);
    assert (parent != null);
    if (parent == null)
    {
      mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(mMainDoc), "Could not find element %1$s.", new Object[] { parentPath });
      
      return false;
    }
    boolean success = true;
    for (Element src : findElements(libDoc, path))
    {
      String name = getAttributeValue(src, keyAttr);
      if (name.length() == 0)
      {
        mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(src), "Undefined '%1$s' attribute in %2$s.", new Object[] { keyAttr, path });
        
        success = false;
      }
      else
      {
        List<Element> dests = findElements(mMainDoc, path, keyAttr, name);
        if (dests.size() > 1) {
          mLog.error(IMergerLog.Severity.WARNING, xmlFileAndLine((Node)dests.get(0)), "Manifest has more than one %1$s[@%2$s=%3$s] element.", new Object[] { path, keyAttr, name });
        }
        boolean doMerge = true;
        Iterator i$ = dests.iterator();
        for (;;)
        {
          if (i$.hasNext())
          {
            Element dest = (Element)i$.next();
            if (hasOverrideOrRemoveTag(dest))
            {
              doMerge = false;
            }
            else
            {
              StringBuilder diff = new StringBuilder();
              if (compareElements(dest, src, false, diff, keyAttr))
              {
                if (!warnDups) {
                  break;
                }
                mLog.conflict(IMergerLog.Severity.INFO, xmlFileAndLine(dest), xmlFileAndLine(src), "Skipping identical %1$s[@%2$s=%3$s] element.", new Object[] { path, keyAttr, name }); break;
              }
              mLog.conflict(IMergerLog.Severity.ERROR, xmlFileAndLine(dest), xmlFileAndLine(src), "Trying to merge incompatible %1$s[@%2$s=%3$s] element:\n%4$s", new Object[] { path, keyAttr, name, diff.toString() });
              
              success = false;
              break;
            }
          }
        }
        if (doMerge)
        {
          Node start = selectPreviousSiblings(src);
          
          insertAtEndOf(parent, start, src);
        }
      }
    }
    return success;
  }
  
  private String getAttributeValue(Element element, String attrName)
  {
    Attr attr = element.getAttributeNodeNS("http://schemas.android.com/apk/res/android", attrName);
    String value = attr == null ? "" : attr.getNodeValue();
    return value;
  }
  
  private boolean mergeAdjustRequired(String path, String keyAttr, String requiredAttr, Document libDoc, @Nullable String alternateKeyAttr)
  {
    int pos = path.lastIndexOf('/');
    assert (pos > 1);
    String parentPath = path.substring(0, pos);
    Element parent = findFirstElement(mMainDoc, parentPath);
    assert (parent != null);
    if (parent == null)
    {
      mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(mMainDoc), "Could not find element %1$s.", new Object[] { parentPath });
      
      return false;
    }
    boolean success = true;
    for (Element src : findElements(libDoc, path))
    {
      Attr attr = src.getAttributeNodeNS("http://schemas.android.com/apk/res/android", keyAttr);
      String name = attr == null ? "" : attr.getNodeValue().trim();
      if (name.length() == 0)
      {
        if (alternateKeyAttr != null)
        {
          attr = src.getAttributeNodeNS("http://schemas.android.com/apk/res/android", alternateKeyAttr);
          String s = attr == null ? "" : attr.getNodeValue().trim();
          if (s.length() != 0) {}
        }
        else
        {
          mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(src), "Undefined '%1$s' attribute in %2$s.", new Object[] { keyAttr, path });
          
          success = false;
        }
      }
      else
      {
        List<Element> dests = findElements(mMainDoc, path, keyAttr, name);
        if (dests.size() > 1) {
          mLog.error(IMergerLog.Severity.WARNING, xmlFileAndLine((Node)dests.get(0)), "Manifest has more than one %1$s[@%2$s=%3$s] element.", new Object[] { path, keyAttr, name });
        }
        String value;
        if (dests.size() > 0)
        {
          attr = src.getAttributeNodeNS("http://schemas.android.com/apk/res/android", requiredAttr);
          value = attr == null ? "true" : attr.getNodeValue();
          if ((value == null) || ((!value.equals("true")) && (!value.equals("false"))))
          {
            mLog.error(IMergerLog.Severity.WARNING, xmlFileAndLine(src), "Invalid attribute '%1$s' in %2$s[@%3$s=%4$s] element:\nExpected 'true' or 'false' but found '%5$s'.", new Object[] { requiredAttr, path, keyAttr, name, value });
          }
          else
          {
            boolean boolE = Boolean.parseBoolean(value);
            for (Element dest : dests) {
              if (!hasOverrideOrRemoveTag(dest))
              {
                attr = dest.getAttributeNodeNS("http://schemas.android.com/apk/res/android", requiredAttr);
                value = attr == null ? "true" : attr.getNodeValue();
                if ((value == null) || ((!value.equals("true")) && (!value.equals("false"))))
                {
                  mLog.error(IMergerLog.Severity.WARNING, xmlFileAndLine(dest), "Invalid attribute '%1$s' in %2$s[@%3$s=%4$s] element:\nExpected 'true' or 'false' but found '%5$s'.", new Object[] { requiredAttr, path, keyAttr, name, value });
                }
                else
                {
                  boolean boolD = Boolean.parseBoolean(value);
                  if ((!boolD) && (boolE)) {
                    if (attr != null) {
                      attr.setNodeValue("true");
                    }
                  }
                }
              }
            }
          }
        }
        else
        {
          Node start = selectPreviousSiblings(src);
          
          Node node = insertAtEndOf(parent, start, src);
          
          NamedNodeMap attrs = node.getAttributes();
          if (attrs != null) {
            for (int i = 0; i < attrs.getLength(); i++)
            {
              Node a = attrs.item(i);
              if (a.getNodeType() == 2)
              {
                boolean keep = "http://schemas.android.com/apk/res/android".equals(a.getNamespaceURI());
                if (keep)
                {
                  name = a.getLocalName();
                  keep = (keyAttr.equals(name)) || (requiredAttr.equals(name));
                }
                if (!keep)
                {
                  attrs.removeNamedItemNS("http://schemas.android.com/apk/res/android", name);
                  
                  i = -1;
                }
              }
            }
          }
        }
      }
    }
    return success;
  }
  
  private boolean checkGlEsVersion(Document libDoc)
  {
    String parentPath = "/manifest";
    Element parent = findFirstElement(mMainDoc, parentPath);
    assert (parent != null);
    if (parent == null)
    {
      mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(mMainDoc), "Could not find element %1$s.", new Object[] { parentPath });
      
      return false;
    }
    String path = "/manifest/uses-feature";
    String keyAttr = "glEsVersion";
    long destGlEsVersion = 65536L;
    Element destNode = null;
    boolean result = true;
    for (Element dest : findElements(mMainDoc, path))
    {
      Attr attr = dest.getAttributeNodeNS("http://schemas.android.com/apk/res/android", keyAttr);
      String value = attr == null ? "" : attr.getNodeValue().trim();
      if (value.length() != 0) {
        try
        {
          long version = Long.decode(value).longValue();
          if (version >= destGlEsVersion)
          {
            destGlEsVersion = version;
            destNode = dest;
          }
          else if (version < 65536L)
          {
            mLog.error(IMergerLog.Severity.WARNING, xmlFileAndLine(dest), "Ignoring <uses-feature android:glEsVersion='%1$s'> because it's smaller than 1.0.", new Object[] { value });
          }
        }
        catch (NumberFormatException e)
        {
          mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(dest), "Failed to parse <uses-feature android:glEsVersion='%1$s'>: must be an integer in the form 0x00020001.", new Object[] { value });
          
          result = false;
        }
      }
    }
    if ((!result) && (destNode == null)) {
      return false;
    }
    long srcGlEsVersion = 65536L;
    Element srcNode = null;
    result = true;
    for (Element src : findElements(libDoc, path))
    {
      Attr attr = src.getAttributeNodeNS("http://schemas.android.com/apk/res/android", keyAttr);
      String value = attr == null ? "" : attr.getNodeValue().trim();
      if (value.length() != 0) {
        try
        {
          long version = Long.decode(value).longValue();
          if (version >= srcGlEsVersion)
          {
            srcGlEsVersion = version;
            srcNode = src;
          }
          else if (version < 65536L)
          {
            mLog.error(IMergerLog.Severity.WARNING, xmlFileAndLine(src), "Ignoring <uses-feature android:glEsVersion='%1$s'> because it's smaller than 1.0.", new Object[] { value });
          }
        }
        catch (NumberFormatException e)
        {
          mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(src), "Failed to parse <uses-feature android:glEsVersion='%1$s'>: must be an integer in the form 0x00020001.", new Object[] { value });
          
          result = false;
        }
      }
    }
    if ((srcNode != null) && (destGlEsVersion < srcGlEsVersion))
    {
      mLog.conflict(IMergerLog.Severity.WARNING, xmlFileAndLine(destNode == null ? mMainDoc : destNode), xmlFileAndLine(srcNode), "Main manifest has <uses-feature android:glEsVersion='0x%1$08x'> but library uses glEsVersion='0x%2$08x'%3$s", new Object[] { Long.valueOf(destGlEsVersion), Long.valueOf(srcGlEsVersion), destNode != null ? "" : "\nNote: main manifest lacks a <uses-feature android:glEsVersion> declaration, and thus defaults to glEsVersion=0x00010000." });
      
      result = false;
    }
    return result;
  }
  
  private boolean checkSdkVersion(Document libDoc)
  {
    boolean result = true;
    
    Element destUsesSdk = findFirstElement(mMainDoc, "/manifest/uses-sdk");
    if (hasOverrideOrRemoveTag(destUsesSdk)) {
      return true;
    }
    Element srcUsesSdk = findFirstElement(libDoc, "/manifest/uses-sdk");
    
    AtomicInteger destValue = new AtomicInteger(1);
    AtomicInteger srcValue = new AtomicInteger(1);
    AtomicBoolean destImplied = new AtomicBoolean(true);
    AtomicBoolean srcImplied = new AtomicBoolean(true);
    
    int destMinSdk = 1;
    result = extractSdkVersionAttribute(libDoc, destUsesSdk, srcUsesSdk, "min", destValue, srcValue, destImplied, srcImplied);
    if (result)
    {
      destMinSdk = destValue.get();
      if (destMinSdk < srcValue.get())
      {
        mLog.conflict(IMergerLog.Severity.ERROR, xmlFileAndLine(destUsesSdk == null ? mMainDoc : destUsesSdk), xmlFileAndLine(srcUsesSdk == null ? libDoc : srcUsesSdk), "Main manifest has <uses-sdk android:minSdkVersion='%1$d'> but library uses minSdkVersion='%2$d'%3$s", new Object[] { Integer.valueOf(destMinSdk), Integer.valueOf(srcValue.get()), !destImplied.get() ? "" : "\nNote: main manifest lacks a <uses-sdk android:minSdkVersion> declaration, which defaults to value 1." });
        
        result = false;
      }
    }
    destImplied.set(true);
    srcImplied.set(true);
    
    boolean result2 = extractSdkVersionAttribute(libDoc, destUsesSdk, srcUsesSdk, "target", destValue, srcValue, destImplied, srcImplied);
    
    result &= result2;
    if (result2)
    {
      int destTargetSdk = destImplied.get() ? destMinSdk : destValue.get();
      if (destTargetSdk < srcValue.get())
      {
        mLog.conflict(IMergerLog.Severity.WARNING, xmlFileAndLine(destUsesSdk == null ? mMainDoc : destUsesSdk), xmlFileAndLine(srcUsesSdk == null ? libDoc : srcUsesSdk), "Main manifest has <uses-sdk android:targetSdkVersion='%1$d'> but library uses targetSdkVersion='%2$d'%3$s", new Object[] { Integer.valueOf(destTargetSdk), Integer.valueOf(srcValue.get()), !destImplied.get() ? "" : "\nNote: main manifest lacks a <uses-sdk android:targetSdkVersion> declaration, which defaults to value minSdkVersion or 1." });
        
        result = false;
      }
    }
    return result;
  }
  
  private boolean extractSdkVersionAttribute(Document libDoc, Element destUsesSdk, Element srcUsesSdk, String attr, AtomicInteger destValue, AtomicInteger srcValue, AtomicBoolean destImplied, AtomicBoolean srcImplied)
  {
    String s = destUsesSdk == null ? "" : destUsesSdk.getAttributeNS("http://schemas.android.com/apk/res/android", attr + "SdkVersion");
    
    boolean result = true;
    assert (s != null);
    s = s.trim();
    try
    {
      if (s.length() > 0)
      {
        destValue.set(Integer.parseInt(s));
        destImplied.set(false);
      }
    }
    catch (NumberFormatException e)
    {
      boolean error = true;
      if (mCallback != null)
      {
        int apiLevel = mCallback.queryCodenameApiLevel(s);
        if (apiLevel > 0)
        {
          destValue.set(apiLevel);
          destImplied.set(false);
          error = false;
        }
      }
      if (error)
      {
        mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(destUsesSdk == null ? mMainDoc : destUsesSdk), "Failed to parse <uses-sdk %1$sSdkVersion='%2$s'>: must be an integer number or codename.", new Object[] { attr, s });
        
        result = false;
      }
    }
    s = srcUsesSdk == null ? "" : srcUsesSdk.getAttributeNS("http://schemas.android.com/apk/res/android", attr + "SdkVersion");
    
    assert (s != null);
    s = s.trim();
    try
    {
      if (s.length() > 0)
      {
        srcValue.set(Integer.parseInt(s));
        srcImplied.set(false);
      }
    }
    catch (NumberFormatException e)
    {
      boolean error = true;
      if (mCallback != null)
      {
        int apiLevel = mCallback.queryCodenameApiLevel(s);
        if (apiLevel > 0)
        {
          srcValue.set(apiLevel);
          srcImplied.set(false);
          error = false;
        }
      }
      if (error)
      {
        mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(srcUsesSdk == null ? libDoc : srcUsesSdk), "Failed to parse <uses-sdk %1$sSdkVersion='%2$s'>: must be an integer number or codename.", new Object[] { attr, s });
        
        result = false;
      }
    }
    return result;
  }
  
  @NonNull
  private Node selectPreviousSiblings(Node end)
  {
    Node start = end;
    Node prev = start.getPreviousSibling();
    while (prev != null)
    {
      short t = prev.getNodeType();
      if (t == 3)
      {
        String text = prev.getNodeValue();
        if ((text == null) || (text.trim().length() != 0)) {
          break;
        }
      }
      else
      {
        if (t != 8) {
          break;
        }
      }
      start = prev;
      prev = start.getPreviousSibling();
    }
    return start;
  }
  
  private Node insertAtEndOf(Element dest, Node start, Node end)
  {
    String destPrefix = XmlUtils.lookupNamespacePrefix(mMainDoc, "http://schemas.android.com/apk/res/android");
    String srcPrefix = XmlUtils.lookupNamespacePrefix(start.getOwnerDocument(), "http://schemas.android.com/apk/res/android");
    boolean needPrefixChange = (destPrefix != null) && (!destPrefix.equals(srcPrefix));
    
    Node target = dest.getLastChild();
    while ((target != null) && 
      (target.getNodeType() == 3))
    {
      String text = target.getNodeValue();
      if ((text == null) || (text.trim().length() != 0)) {
        break;
      }
      target = target.getPreviousSibling();
    }
    if (target != null) {
      target = target.getNextSibling();
    }
    assert (dest.getOwnerDocument() == mMainDoc);
    assert (dest.getOwnerDocument() != start.getOwnerDocument());
    assert (start.getOwnerDocument() == end.getOwnerDocument());
    while (start != null)
    {
      Node node = mMainDoc.importNode(start, true);
      if (needPrefixChange) {
        changePrefix(node, srcPrefix, destPrefix);
      }
      if (mInsertSourceMarkers)
      {
        File file = MergerXmlUtils.getFileFor(start);
        if (file != null) {
          MergerXmlUtils.setFileFor(node, file);
        }
      }
      dest.insertBefore(node, target);
      if (start == end) {
        return node;
      }
      start = start.getNextSibling();
    }
    return null;
  }
  
  private void changePrefix(Node node, String srcPrefix, String destPrefix)
  {
    for (; node != null; node = node.getNextSibling())
    {
      if (srcPrefix.equals(node.getPrefix())) {
        node.setPrefix(destPrefix);
      }
      Node child = node.getFirstChild();
      if (child != null) {
        changePrefix(child, srcPrefix, destPrefix);
      }
    }
  }
  
  private boolean compareElements(@NonNull Node expected, @NonNull Node actual, boolean nextSiblings, @Nullable StringBuilder diff, @Nullable String keyAttr)
  {
    Map<String, String> nsPrefixE = new HashMap();
    Map<String, String> nsPrefixA = new HashMap();
    String sE = MergerXmlUtils.printElement(expected, nsPrefixE, "");
    String sA = MergerXmlUtils.printElement(actual, nsPrefixA, "");
    if (sE.equals(sA)) {
      return true;
    }
    if (diff != null) {
      MergerXmlUtils.printXmlDiff(diff, sE, sA, nsPrefixE, nsPrefixA, "http://schemas.android.com/apk/res/android:" + keyAttr);
    }
    return false;
  }
  
  @Nullable
  private Element findFirstElement(@NonNull Document doc, @NonNull String path)
  {
    try
    {
      Node result = (Node)mXPath.evaluate(path, doc, XPathConstants.NODE);
      if ((result instanceof Element)) {
        return (Element)result;
      }
      if (result != null) {
        mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(doc), "Unexpected Node type %s when evaluating %s", new Object[] { result.getClass().getName(), path });
      }
    }
    catch (XPathExpressionException e)
    {
      mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(doc), "XPath error on expr %s: %s", new Object[] { path, e.toString() });
    }
    return null;
  }
  
  private List<Element> findElements(@NonNull Document doc, @NonNull String path)
  {
    return findElements(doc, path, null, null);
  }
  
  private List<Element> findElements(@NonNull Document doc, @NonNull String path, @Nullable String attrName, @Nullable String attrValue)
  {
    List<Element> elements = new ArrayList();
    if (attrName != null)
    {
      assert (attrValue != null);
      
      path = String.format("%1$s[@%2$s:%3$s='%4$s']", new Object[] { path, "android", attrName, attrValue });
    }
    try
    {
      NodeList results = (NodeList)mXPath.evaluate(path, doc, XPathConstants.NODESET);
      if ((results != null) && (results.getLength() > 0)) {
        for (int i = 0; i < results.getLength(); i++)
        {
          Node n = results.item(i);
          assert ((n instanceof Element));
          if ((n instanceof Element)) {
            elements.add((Element)n);
          } else {
            mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(doc), "Unexpected Node type %s when evaluating %s", new Object[] { n.getClass().getName(), path });
          }
        }
      }
    }
    catch (XPathExpressionException e)
    {
      mLog.error(IMergerLog.Severity.ERROR, xmlFileAndLine(doc), "XPath error on expr %s: %s", new Object[] { path, e.toString() });
    }
    return elements;
  }
  
  @NonNull
  private IMergerLog.FileAndLine xmlFileAndLine(@NonNull Node node)
  {
    return MergerXmlUtils.xmlFileAndLine(node);
  }
  
  private boolean hasOverrideOrRemoveTag(@Nullable Node node)
  {
    if ((node == null) || (node.getNodeType() != 1)) {
      return false;
    }
    NamedNodeMap attrs = node.getAttributes();
    Node merge = attrs.getNamedItemNS("http://schemas.android.com/tools", "merge");
    String value = merge == null ? null : merge.getNodeValue();
    return ("override".equals(value)) || ("remove".equals(value));
  }
  
  private void cleanupToolsAttributes(@Nullable Node root)
  {
    if (root == null) {
      return;
    }
    NamedNodeMap attrs = root.getAttributes();
    if (attrs != null)
    {
      for (int i = attrs.getLength() - 1; i >= 0; i--)
      {
        Node attr = attrs.item(i);
        if (("http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) && ("http://schemas.android.com/tools".equals(attr.getNodeValue()))) {
          attrs.removeNamedItem(attr.getNodeName());
        } else if (("http://schemas.android.com/tools".equals(attr.getNamespaceURI())) && ("merge".equals(attr.getLocalName()))) {
          attrs.removeNamedItem(attr.getNodeName());
        }
      }
      assert (attrs.getNamedItemNS("http://schemas.android.com/tools", "merge") == null);
    }
    for (Node child = root.getFirstChild(); child != null;) {
      if (child.getNodeType() != 1)
      {
        child = child.getNextSibling();
      }
      else
      {
        attrs = child.getAttributes();
        Node merge = attrs == null ? null : attrs.getNamedItemNS("http://schemas.android.com/tools", "merge");
        String value = merge == null ? null : merge.getNodeValue();
        Node sibling = child.getNextSibling();
        if ("remove".equals(value))
        {
          Node prev = child.getPreviousSibling();
          root.removeChild(child);
          while ((prev != null) && (prev.getNodeType() == 3) && 
            (prev.getNodeValue().trim().length() == 0))
          {
            Node prevPrev = prev.getPreviousSibling();
            root.removeChild(prev);
            prev = prevPrev;
          }
        }
        else
        {
          cleanupToolsAttributes(child);
        }
        child = sibling;
      }
    }
  }
  
  private Document cleanupToolsAttributes(@NonNull Document doc)
  {
    cleanupToolsAttributes(doc.getFirstChild());
    return doc;
  }
  
  public void setInsertSourceMarkers(boolean insertSourceMarkers)
  {
    mInsertSourceMarkers = insertSourceMarkers;
  }
  
  public boolean isInsertSourceMarkers()
  {
    return mInsertSourceMarkers;
  }
  
  private static void insertSourceMarkers(@NonNull Document mainDoc)
  {
    Element root = mainDoc.getDocumentElement();
    if (root != null)
    {
      File file = MergerXmlUtils.getFileFor(root);
      if (file != null) {
        insertSourceMarker(mainDoc, root, file, false);
      }
      insertSourceMarkers(root, file);
    }
1 2

Further reading...

For more information on Java 1.5 Tiger, you may find Java 1.5 Tiger, A developer's Notebook by D. Flanagan and B. McLaughlin from O'Reilly of interest.

New!JAR listings


Copyright 2006-2017. Infinite Loop Ltd