/*
 * Copyright (C) 2015 The Pennsylvania State University and the University of Wisconsin
 * Systems and Internet Infrastructure Security Laboratory
 *
 * Author: Damien Octeau
 *
 * 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 edu.psu.cse.siis.ic3.manifest;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import edu.psu.cse.siis.ic3.Ic3Data;
import edu.psu.cse.siis.ic3.Ic3Data.Application.Builder;
import edu.psu.cse.siis.ic3.Ic3Data.Application.Component;
import edu.psu.cse.siis.ic3.Ic3Data.Application.Component.ComponentKind;
import edu.psu.cse.siis.ic3.Ic3Data.Application.Component.IntentFilter;
import edu.psu.cse.siis.ic3.Ic3Data.Attribute;
import edu.psu.cse.siis.ic3.Ic3Data.AttributeKind;
import edu.psu.cse.siis.ic3.db.Constants;
import edu.psu.cse.siis.ic3.db.SQLConnection;
import edu.psu.cse.siis.ic3.manifest.binary.AXmlResourceParser;

public class ManifestPullParser {
  private static final String MANIFEST = "manifest";
  private static final String MANIFEST_FILE_NAME = "AndroidManifest.xml";
  private static final String ACTIVITY = "activity";
  private static final String ACTIVITY_ALIAS = "activity-alias";
  private static final String SERVICE = "service";
  private static final String PROVIDER = "provider";
  private static final String AUTHORITIES = "authorities";
  private static final String READ_PERMISSION = "readPermissions";
  private static final String WRITE_PERMISSION = "writePermission";
  private static final String RECEIVER = "receiver";
  private static final String APPLICATION = "application";
  private static final String NAMESPACE = "http://schemas.android.com/apk/res/android";
  // private static final String ENABLED = "android:enabled";
  private static final String NAME = "name";
  private static final String INTENT_FILTER = "intent-filter";
  private static final String ACTION = "action";
  private static final String CATEGORY = "category";
  private static final String MIME_TYPE = "mimeType";
  private static final String PACKAGE = "package";
  private static final String DATA = "data";
  private static final String FALSE = "false";
  private static final String VERSION = "versionCode";
  private static final String PERMISSION = "permission";
  private static final String EXPORTED = "exported";
  private static final String TRUE = "true";
  private static final String USES_PERMISSION = "uses-permission";
  private static final String PROTECTION_LEVEL = "protectionLevel";
  private static final String TARGET_ACTIVITY = "targetActivity";
  private static final String SCHEME = "scheme";
  private static final String HOST = "host";
  private static final String PORT = "port";
  private static final String PATH = "path";
  private static final String PATH_PATTERN = "pathPattern";
  private static final String PATH_PREFIX = "pathPrefix";
  private static final String GRANT_URI_PERMISSIONS = "grantUriPermissions";
  private static final String PRIORITY = "priority";

  private static final String NORMAL = "normal";
  private static final String DANGEROUS = "dangerous";
  private static final String SIGNATURE = "signature";
  private static final String SIGNATURE_OR_SYSTEM = "signatureOrSytem";

  private String applicationName;
  private String packageName;

  private static final String[] levelValueToShortString = { Constants.PermissionLevel.NORMAL_SHORT,
      Constants.PermissionLevel.DANGEROUS_SHORT, Constants.PermissionLevel.SIGNATURE_SHORT,
      Constants.PermissionLevel.SIGNATURE_OR_SYSTEM_SHORT };

  private static Map<String, Integer> tagDepthMap = null;

  // map a content provider to one or more authorities
  // private final Map<String, Set<String>> providersAuthorities = new HashMap<String,
  // Set<String>>();
  // private final Map<String, Set<String>> providersRPermission = new HashMap<String,
  // Set<String>>();
  // private final Map<String, Set<String>> providersWPermission = new HashMap<String,
  // Set<String>>();

  private final List<ManifestComponent> activities = new ArrayList<ManifestComponent>();
  private final List<ManifestComponent> activityAliases = new ArrayList<ManifestComponent>();
  private final List<ManifestComponent> services = new ArrayList<ManifestComponent>();
  private final List<ManifestComponent> receivers = new ArrayList<ManifestComponent>();
  private final List<ManifestComponent> providers = new ArrayList<ManifestComponent>();
  private int version = -1;
  private ManifestComponent currentComponent = null;
  private Set<ManifestIntentFilter> currentIntentFilters = null;
  private ManifestIntentFilter currentIntentFilter = null;
  private String skipToEndTag = null;
  private String applicationPermission = null;
  private final Set<String> usesPermissions = new HashSet<>();
  private Map<String, String> permissions = new HashMap<String, String>();
  private final Set<String> entryPointClasses = new HashSet<>();

  public List<ManifestComponent> getActivities() {
    return activities;
  }

  public List<ManifestComponent> getActivityAliases() {
    return activityAliases;
  }

  public List<ManifestComponent> getServices() {
    return services;
  }

  public List<ManifestComponent> getReceivers() {
    return receivers;
  }

  public List<ManifestComponent> getProviders() {
    return providers;
  }

  public List<String> getComponents() {
    List<String> componentNames = new ArrayList<>();
    addComponentNamesToList(activities, componentNames);
    addComponentNamesToList(services, componentNames);
    addComponentNamesToList(receivers, componentNames);
    addComponentNamesToList(providers, componentNames);

    return componentNames;
  }

  private void setApplicationName(String applicationName) {
    this.applicationName = applicationName;
  }

  public String getApplicationName() {
    return applicationName;
  }

  private void setPackageName(String packageName) {
    this.packageName = packageName;
  }

  public String getPackageName() {
    return packageName;
  }

  public Set<String> getEntryPointClasses() {
    return entryPointClasses;
  }

  private void addComponentNamesToList(List<ManifestComponent> components, List<String> output) {
    for (ManifestComponent manifestComponent : components) {
      output.add(manifestComponent.getName());
    }
  }

  public void loadManifestFile(String manifest) {
    try {
      if (manifest.endsWith(".xml")) {
        loadClassesFromTextManifest(new FileInputStream(manifest));
      } else {
        handleBinaryManifestFile(manifest);
      }
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
  }

  private void handleBinaryManifestFile(String apk) {
    try {
      ZipFile archive = new ZipFile(apk);
      ZipEntry manifestEntry = archive.getEntry(MANIFEST_FILE_NAME);
      if (manifestEntry == null) {
        archive.close();
        throw new RuntimeException("No manifest file found in apk");
      }
      loadClassesFromBinaryManifest(archive.getInputStream(manifestEntry));
      archive.close();
    } catch (IOException e) {
      throw new RuntimeException("Error while processing apk " + apk + ": " + e);
    }
  }

  protected void loadClassesFromBinaryManifest(InputStream manifestIS) {
    AXmlResourceParser aXmlResourceParser = new AXmlResourceParser();
    aXmlResourceParser.open(manifestIS);
    try {
      parse(aXmlResourceParser);
    } catch (XmlPullParserException | IOException e) {
      e.printStackTrace();
      throw new RuntimeException("Could not parse manifest file.");
    }
  }

  protected void loadClassesFromTextManifest(InputStream manifestIS) {
    try {
      XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
      xmlPullParserFactory.setNamespaceAware(true);
      XmlPullParser parser = xmlPullParserFactory.newPullParser();
      parser.setInput(manifestIS, null);
      parse(parser);
    } catch (XmlPullParserException | IOException e) {
      e.printStackTrace();
      throw new RuntimeException("Could not parse manifest file.");
    }
  }

  /**
   * Parse the manifest file.
   *
   * @param reader Input if text XML.
   * @param is Input if binary XML.
   * @throws IOException
   * @throws XmlPullParserException
   */
  public void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
    initializeTagDepthMap();

    int depth = 0;
    int eventType = 0;
    eventType = parser.next();

    boolean okayToContinue = true;

    while (okayToContinue && eventType != XmlPullParser.END_DOCUMENT) {
      switch (eventType) {
        case XmlPullParser.START_TAG:
          okayToContinue = handleStartTag(parser, depth);
          ++depth;
          break;
        case XmlPullParser.END_TAG:
          okayToContinue = handleEndTag(parser);
          --depth;
          break;
      }
      eventType = parser.next();
    }
  }

  public Map<String, Integer> writeToDb(boolean skipEntryPoints) {
    Map<String, Integer> componentIds = new HashMap<String, Integer>();

    componentIds.putAll(SQLConnection.insert(getPackageName(), version, activities,
        usesPermissions, permissions, skipEntryPoints));
    componentIds.putAll(SQLConnection.insert(getPackageName(), version, activityAliases, null,
        null, skipEntryPoints));
    componentIds.putAll(SQLConnection.insert(getPackageName(), version, services, null, null,
        skipEntryPoints));
    componentIds.putAll(SQLConnection.insert(getPackageName(), version, receivers, null, null,
        skipEntryPoints));
    componentIds.putAll(SQLConnection.insert(getPackageName(), version, providers, null, null,
        skipEntryPoints));

    return componentIds;
  }

  public boolean isComponent(String name) {
    return entryPointClasses.contains(name);
  }

  public Map<String, Ic3Data.Application.Component.Builder> populateProtobuf(Builder ic3Builder) {
    ic3Builder.setName(getPackageName());
    ic3Builder.setVersion(version);

    for (Map.Entry<String, String> permission : permissions.entrySet()) {
      Ic3Data.Application.Permission protobufPermission =
          Ic3Data.Application.Permission.newBuilder().setName(permission.getKey())
              .setLevel(stringToLevel(permission.getValue())).build();
      ic3Builder.addPermissions(protobufPermission);
    }

    ic3Builder.addAllUsedPermissions(usesPermissions);

    Map<String, Ic3Data.Application.Component.Builder> componentNameToBuilderMap = new HashMap<>();

    componentNameToBuilderMap.putAll(populateProtobufComponentBuilders(activities,
        ComponentKind.ACTIVITY));
    componentNameToBuilderMap.putAll(populateProtobufComponentBuilders(services,
        ComponentKind.SERVICE));
    componentNameToBuilderMap.putAll(populateProtobufComponentBuilders(receivers,
        ComponentKind.RECEIVER));
    componentNameToBuilderMap.putAll(populateProtobufComponentBuilders(providers,
        ComponentKind.PROVIDER));

    return componentNameToBuilderMap;
  }

  private Map<String, Component.Builder> populateProtobufComponentBuilders(
      List<ManifestComponent> components, ComponentKind componentKind) {
    Map<String, Component.Builder> componentNameToBuilderMap = new HashMap<>();

    for (ManifestComponent manifestComponent : components) {
      componentNameToBuilderMap.put(manifestComponent.getName(),
          makeProtobufComponentBuilder(manifestComponent, componentKind));
    }

    return componentNameToBuilderMap;
  }

  public static Component.Builder makeProtobufComponentBuilder(ManifestComponent manifestComponent,
      ComponentKind componentKind) {
    Component.Builder componentBuilder = Component.newBuilder();
    componentBuilder.setName(manifestComponent.getName());
    componentBuilder.setKind(componentKind);

    componentBuilder.setExported(manifestComponent.isExported());
    if (manifestComponent.getPermission() != null) {
      componentBuilder.setPermission(manifestComponent.getPermission());
    }

    if (manifestComponent.missingIntentFilters() != null) {
      componentBuilder.setMissing(manifestComponent.missingIntentFilters());
    }

    if (manifestComponent.getIntentFilters() != null) {
      for (ManifestIntentFilter filter : manifestComponent.getIntentFilters()) {
        IntentFilter.Builder filterBuilder = IntentFilter.newBuilder();
        if (filter.getPriority() != null) {
          filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.PRIORITY)
              .addIntValue(filter.getPriority()));
        }
        Set<String> value = filter.getActions();
        if (value != null) {
          if (value.contains(null)) {
            value.remove(null);
            value.add(edu.psu.cse.siis.coal.Constants.NULL_STRING);
          }
          filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.ACTION)
              .addAllValue(value));
        }
        value = filter.getCategories();
        if (value != null) {
          if (value.contains(null)) {
            value.remove(null);
            value.add(edu.psu.cse.siis.coal.Constants.NULL_STRING);
          }
          filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.CATEGORY)
              .addAllValue(value));
        }
        if (filter.getData() != null) {
          for (ManifestData data : filter.getData()) {
            if (data.getHost() != null) {
              filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.HOST)
                  .addValue(data.getHost()));
            }
            if (data.getMimeType() != null) {
              // String[] typeParts = data.getMimeType().split("/");
              // String type;
              // String subtype;
              // if (typeParts.length == 2) {
              // type = typeParts[0];
              // subtype = typeParts[1];
              // } else {
              // type = Constants.ANY_STRING;
              // subtype = Constants.ANY_STRING;
              // }
              filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.TYPE)
                  .addValue(data.getMimeType()));
              // filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.SUBTYPE)
              // .addValue(subtype));
            }
            if (data.getPath() != null) {
              filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.PATH)
                  .addValue(data.getPath()));
            }
            if (data.getPort() != null) {
              filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.PORT)
                  .addValue(data.getPort()));
            }
            if (data.getScheme() != null) {
              filterBuilder.addAttributes(Attribute.newBuilder().setKind(AttributeKind.SCHEME)
                  .addValue(data.getScheme()));
            }
          }
        }

        componentBuilder.addIntentFilters(filterBuilder);
      }
    }

    return componentBuilder;
  }

  private Ic3Data.Application.Permission.Level stringToLevel(String levelString) {
    if (levelString.equalsIgnoreCase(Constants.PermissionLevel.NORMAL_SHORT)) {
      return Ic3Data.Application.Permission.Level.NORMAL;
    } else if (levelString.equalsIgnoreCase(Constants.PermissionLevel.DANGEROUS_SHORT)) {
      return Ic3Data.Application.Permission.Level.DANGEROUS;
    } else if (levelString.equalsIgnoreCase(Constants.PermissionLevel.SIGNATURE_SHORT)) {
      return Ic3Data.Application.Permission.Level.SIGNATURE;
    } else if (levelString.equalsIgnoreCase(Constants.PermissionLevel.SIGNATURE_OR_SYSTEM_SHORT)) {
      return Ic3Data.Application.Permission.Level.SIGNATURE_OR_SYSTEM;
    } else {
      throw new RuntimeException("Unknown permission level: " + levelString);
    }
  }

  @Override
  public String toString() {
    StringBuilder result = new StringBuilder("Manifest file for ");
    result.append(getPackageName());
    if (version != -1) {
      result.append(" version ").append(version);
    }
    result.append("\n  Activities:\n");
    componentMapToString(activities, result);
    result.append("\n  Activity Aliases:\n");
    componentMapToString(activityAliases, result);
    result.append("  Services:\n");
    componentMapToString(services, result);
    result.append("  Receivers:\n");
    componentMapToString(receivers, result);

    result.append("  Providers:\n");
    componentMapToString(providers, result);

    return result.toString();
  }

  private void componentMapToString(List<ManifestComponent> components, StringBuilder out) {
    for (ManifestComponent component : components) {
      out.append("    ").append(component.getName()).append("\n");
      if (component.getIntentFilters() != null) {
        for (ManifestIntentFilter manifestIntentFilter : component.getIntentFilters()) {
          out.append(manifestIntentFilter.toString("      "));
        }
      }
      if (component instanceof ManifestProviderComponent) {
        ManifestProviderComponent provider = (ManifestProviderComponent) component;
        for (String authority : provider.getAuthorities()) {
          out.append("      authority: " + authority + "\n");
        }
        String readPermission = provider.getReadPermission();
        String writePermission = provider.getWritePermission();
        if (readPermission != null && !readPermission.equals("")) {
          out.append("      read permission: " + readPermission + "\n");
        }
        if (writePermission != null && !writePermission.equals("")) {
          out.append("      write permission: " + writePermission + "\n");
        }
      }
    }
  }

  private boolean handleEndTag(XmlPullParser parser) {
    String tagName = parser.getName();

    if (skipToEndTag != null) {
      if (skipToEndTag.equals(tagName)) {
        skipToEndTag = null;
      }
      return true;
    }

    if (tagName.equals(ACTIVITY)) {
      return handleActivityEnd(parser);
    }
    if (tagName.equals(ACTIVITY_ALIAS)) {
      return handleActivityAliasEnd(parser);
    }
    if (tagName.equals(SERVICE)) {
      return handleServiceEnd(parser);
    }
    if (tagName.equals(RECEIVER)) {
      return handleReceiverEnd(parser);
    }
    if (tagName.equals(INTENT_FILTER)) {
      return handleIntentFilterEnd(parser);
    }
    if (tagName.equals(PROVIDER)) {
      return handleProviderEnd(parser);
    }

    return true;
  }

  private void initializeTagDepthMap() {
    if (tagDepthMap == null) {
      tagDepthMap = new HashMap<String, Integer>();
      tagDepthMap.put(MANIFEST, 0);
      tagDepthMap.put(USES_PERMISSION, 1);
      tagDepthMap.put(PERMISSION, 1);
      tagDepthMap.put(APPLICATION, 1);
      tagDepthMap.put(ACTIVITY, 2);
      tagDepthMap.put(SERVICE, 2);
      tagDepthMap.put(RECEIVER, 2);
      tagDepthMap.put(INTENT_FILTER, 3);
      tagDepthMap.put(ACTION, 4);
      tagDepthMap.put(CATEGORY, 4);
      tagDepthMap.put(DATA, 4);

      tagDepthMap = Collections.unmodifiableMap(tagDepthMap);
    }
  }

  private boolean handleStartTag(XmlPullParser parser, int depth) {
    if (skipToEndTag != null) {
      return true;
    }

    String tagName = parser.getName();
    if (!checkDepth(tagName, depth)) {
      return true;
    }

    if (tagName.equals(ACTIVITY)) {
      return handleActivityStart(parser);
    }
    if (tagName.equals(ACTIVITY_ALIAS)) {
      return handleActivityAliasStart(parser);
    }
    if (tagName.equals(SERVICE)) {
      return handleServiceStart(parser);
    }
    if (tagName.equals(RECEIVER)) {
      return handleReceiverStart(parser);
    }
    if (tagName.equals(INTENT_FILTER)) {
      return handleIntentFilterStart(parser);
    }
    if (tagName.equals(ACTION)) {
      return handleActionStart(parser);
    }
    if (tagName.equals(CATEGORY)) {
      return handleCategoryStart(parser);
    }
    if (tagName.equals(MANIFEST)) {
      return handleManifestStart(parser);
    }
    if (tagName.equals(APPLICATION)) {
      return handleApplicationStart(parser);
    }
    if (tagName.equals(USES_PERMISSION)) {
      return handleUsesPermissionStart(parser);
    }
    if (tagName.equals(PERMISSION)) {
      return handlePermissionStart(parser);
    }
    if (tagName.equals(DATA)) {
      return handleDataStart(parser);
    }
    if (tagName.equals(PROVIDER)) {
      return handleProviderStart(parser);
    }

    return true;
  }

  private boolean handleIntentFilterStart(XmlPullParser parser) {
    Integer priority = null;
    String priorityString = parser.getAttributeValue(NAMESPACE, PRIORITY);
    if (priorityString != null && priorityString.length() != 0) {
      try {
        priority = Integer.valueOf(priorityString);
      } catch (NumberFormatException exception) {
        System.err.println("Bad priority: " + priorityString);
      }
    }
    currentIntentFilter = new ManifestIntentFilter(false, priority);
    return true;
  }

  private boolean handleIntentFilterEnd(XmlPullParser parser) {
    if (currentIntentFilters == null) {
      currentIntentFilters = new HashSet<ManifestIntentFilter>();
    }
    currentIntentFilters.add(currentIntentFilter);
    currentIntentFilter = null;
    return true;
  }

  private boolean handleActivityStart(XmlPullParser parser) {
    return handleComponentStart(parser, ACTIVITY, Constants.ComponentShortType.ACTIVITY);
  }

  private boolean handleActivityEnd(XmlPullParser parser) {
    return handleComponentEnd(parser, activities);
  }

  private boolean handleActivityAliasStart(XmlPullParser parser) {
    return handleComponentStart(parser, ACTIVITY_ALIAS, Constants.ComponentShortType.ACTIVITY);
  }

  private boolean handleActivityAliasEnd(XmlPullParser parser) {
    return handleComponentEnd(parser, activityAliases);
  }

  private boolean handleServiceStart(XmlPullParser parser) {
    return handleComponentStart(parser, SERVICE, Constants.ComponentShortType.SERVICE);
  }

  private boolean handleServiceEnd(XmlPullParser parser) {
    return handleComponentEnd(parser, services);
  }

  private boolean handleReceiverStart(XmlPullParser parser) {
    return handleComponentStart(parser, RECEIVER, Constants.ComponentShortType.RECEIVER);
  }

  private boolean handleReceiverEnd(XmlPullParser parser) {
    return handleComponentEnd(parser, receivers);
  }

  private boolean handleComponentStart(XmlPullParser parser, String endTag, String componentType) {
    String name = null;
    String targetActivity = null;
    boolean isExported = false;
    boolean foundExported = false;
    String permission = null;

    for (int i = 0; i < parser.getAttributeCount(); ++i) {
      if (!parser.getAttributeNamespace(i).equals(NAMESPACE)) {
        continue;
      }
      String attributeName = parser.getAttributeName(i);
      if (attributeName.equals(NAME)) {
        name = parser.getAttributeValue(i);
      } else if (attributeName.equals(EXPORTED)) {
        String value = parser.getAttributeValue(i);
        if (value.equals(TRUE)) {
          isExported = true;
          foundExported = true;
        } else if (value.equals(FALSE)) {
          foundExported = true;
        }
      } else if (attributeName.equals(PERMISSION)) {
        permission = parser.getAttributeValue(i);
      } else if (attributeName.equals(TARGET_ACTIVITY)) {
        targetActivity = parser.getAttributeValue(i);
      }
    }

    if (name == null
        || (targetActivity != null && !entryPointClasses
            .contains(canonicalizeComponentName(targetActivity)))) {
      skipToEndTag = endTag;
      return true;
    }

    if (permission == null) {
      permission = applicationPermission;
    }
    currentComponent =
        new ManifestComponent(componentType, canonicalizeComponentName(name), isExported,
            foundExported, permission, targetActivity, null, null, null);

    return true;
  }

  private boolean handleComponentEnd(XmlPullParser parser, List<ManifestComponent> componentSet) {
    currentComponent.setIntentFiltersAndExported(currentIntentFilters);
    entryPointClasses.add(currentComponent.getName());
    componentSet.add(currentComponent);
    currentComponent = null;
    currentIntentFilters = null;
    return true;
  }

  private boolean handleApplicationStart(XmlPullParser parser) {
    for (int i = 0; i < parser.getAttributeCount(); ++i) {
      if (!parser.getAttributeNamespace(i).equals(NAMESPACE)) {
        continue;
      }
      // The enabled setting can be changed in the code using the PackageManager class.
      if (parser.getAttributeName(i).equals(NAME)) {
        setApplicationName(parser.getAttributeValue(i));
      } else if (parser.getAttributeName(i).equals(PERMISSION)) {
        applicationPermission = parser.getAttributeValue(i);
      }
    }
    return true;
  }

  private boolean handleUsesPermissionStart(XmlPullParser parser) {
    String permission = parser.getAttributeValue(NAMESPACE, NAME);
    if (permission != null) {
      usesPermissions.add(permission);
    }
    return true;
  }

  private boolean handlePermissionStart(XmlPullParser parser) {
    String permission = parser.getAttributeValue(NAMESPACE, NAME);
    String protectionLevel = parser.getAttributeValue(NAMESPACE, PROTECTION_LEVEL);
    protectionLevel = transformProtectionLevel(protectionLevel);
    if (permission != null) {
      if (permissions == null) {
        permissions = new HashMap<String, String>();
      }
      permissions.put(permission, protectionLevel);
    }
    return true;
  }

  /**
   * Transform the protection level to something appropriate for the database.
   *
   * The protection level is stored in two different ways. In a binary manifest, it is an integer.
   * In a text manifest, it is the string designation of the protection level.
   *
   * @param protectionLevel The protection level found in the manifest.
   * @return A string representation for the protection level appropriate for the database.
   */
  private String transformProtectionLevel(String protectionLevel) {
    if (protectionLevel == null || protectionLevel.equals("")
        || NORMAL.equalsIgnoreCase(protectionLevel)) {
      return Constants.PermissionLevel.NORMAL_SHORT;
    } else if (DANGEROUS.equalsIgnoreCase(protectionLevel)) {
      return Constants.PermissionLevel.DANGEROUS_SHORT;
    } else if (SIGNATURE.equalsIgnoreCase(protectionLevel)) {
      return Constants.PermissionLevel.SIGNATURE_SHORT;
    } else if (SIGNATURE_OR_SYSTEM.equalsIgnoreCase(protectionLevel)) {
      return Constants.PermissionLevel.SIGNATURE_OR_SYSTEM_SHORT;
    } else {
      // We are dealing with a binary manifest file.
      if (protectionLevel.startsWith("0x")) {
        // Even if hexadecimal, we still don't care about the radix, since these only go up
        // to 3.
        protectionLevel = protectionLevel.substring(2);
      }
      int level = Integer.parseInt(protectionLevel);
      return levelValueToShortString[level];
    }
  }

  private boolean handleActionStart(XmlPullParser parser) {
    if (currentIntentFilter != null) {
      currentIntentFilter.addAction(parser.getAttributeValue(NAMESPACE, NAME));
    }
    return true;
  }

  private boolean handleCategoryStart(XmlPullParser parser) {
    if (currentIntentFilter != null) {
      currentIntentFilter.addCategory(parser.getAttributeValue(NAMESPACE, NAME));
    }
    return true;
  }

  private boolean handleDataStart(XmlPullParser parser) {
    if (currentIntentFilter != null) {
      ManifestData manifestData = new ManifestData();

      for (int i = 0; i < parser.getAttributeCount(); ++i) {
        if (!parser.getAttributeNamespace(i).equals(NAMESPACE)) {
          continue;
        }
        String attributeName = parser.getAttributeName(i);
        String attributeValue = parser.getAttributeValue(i);
        if (attributeName.equals(MIME_TYPE)) {
          manifestData.setMimeType(attributeValue);
        } else if (attributeName.equals(SCHEME)) {
          manifestData.setScheme(attributeValue);
        } else if (attributeName.equals(HOST)) {
          manifestData.setHost(attributeValue);
        } else if (attributeName.equals(PORT)) {
          manifestData.setPort(attributeValue);
        } else if (attributeName.equals(PATH)) {
          manifestData.setPath(attributeValue);
        } else if (attributeName.equals(PATH_PATTERN)) {
          manifestData.setPath(attributeValue);
        } else if (attributeName.equals(PATH_PREFIX)) {
          manifestData.setPath(String.format("%s(.*)", attributeValue));
        }
      }

      currentIntentFilter.addData(manifestData);
    }

    return true;
  }

  private String canonicalizeComponentName(String name) {
    if (name.length() == 0) {
      throw new RuntimeException("Component should have non-empty name.");
    } else if (!name.contains(".")) {
      // The non-official rule is that we also prefix the name with the package when
      // the component name does not contain any dot.
      // (http://stackoverflow.com/questions/3608017/activity-name-in-androidmanifest-xml)
      name = getPackageName() + "." + name;
    } else if (name.charAt(0) == '.') {
      // The official rule is to prefix the name with the package when the component
      // name starts with a dot.
      // (http://developer.android.com/guide/topics/manifest/activity-element.html#nm)
      name = getPackageName() + name;
    }

    // Found a case where there was a '/' instead of a '.'.
    return name.replace('/', '.');
  }

  private boolean handleManifestStart(XmlPullParser parser) {
    for (int i = 0; i < parser.getAttributeCount(); ++i) {
      String attributeName = parser.getAttributeName(i);
      if (attributeName.equals(PACKAGE)) {
        // No namespace requirement.
        setPackageName(parser.getAttributeValue(i));
      } else if (parser.getAttributeNamespace(i).equals(NAMESPACE) && attributeName.equals(VERSION)) {
        version = Integer.parseInt(parser.getAttributeValue(i));
      }
    }
    if (getPackageName() != null) {
      return true;
    } else {
      return false;
    }
  }

  private boolean handleProviderStart(XmlPullParser parser) {
    boolean r = handleComponentStart(parser, PROVIDER, Constants.ComponentShortType.PROVIDER);
    String readPermission = currentComponent.getPermission();
    String writePermission = currentComponent.getPermission();
    Set<String> authorities = new HashSet<String>();
    boolean grantUriPermissions = false;

    for (int i = 0; i < parser.getAttributeCount(); ++i) {
      if (!parser.getAttributeNamespace(i).equals(NAMESPACE)) {
        continue;
      }
      String attributeName = parser.getAttributeName(i);
      // permissions
      // Note: readPermission and writePermission attributes take precedence over
      // permission attribute
      // (http://developer.android.com/guide/topics/manifest/provider-element.html).
      if (attributeName.equals(READ_PERMISSION)) {
        readPermission = parser.getAttributeValue(i);
      } else if (attributeName.equals(WRITE_PERMISSION)) {
        writePermission = parser.getAttributeValue(i);
      } else if (attributeName.equals(AUTHORITIES)) {
        // the "AUTHORITIES" attribute contains a list of authorities separated by semicolons.
        String s = parser.getAttributeValue(i);
        for (String a : s.split(";")) {
          authorities.add(a);
        }
      } else if (attributeName.equals(GRANT_URI_PERMISSIONS)) {
        grantUriPermissions = !parser.getAttributeValue(i).equals("false");
      }
    }

    currentComponent =
        new ManifestProviderComponent(currentComponent.getType(), currentComponent.getName(),
            currentComponent.isExported(), currentComponent.isFoundExported(), readPermission,
            writePermission, authorities, grantUriPermissions);
    return r;
  }

  private boolean handleProviderEnd(XmlPullParser parser) {
    return handleComponentEnd(parser, providers);
  }

  private boolean checkDepth(String tagName, int depth) {
    Integer expectedDepth = tagDepthMap.get(tagName);
    if (expectedDepth != null && expectedDepth != depth) {
      skipToEndTag = tagName;
      System.err.println("Warning: malformed Manifest file: " + tagName + " at depth " + depth);
    }

    return true;
  }
}
