All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.autonomousapps.internal.ManifestParser.kt Maven / Gradle / Ivy

There is a newer version: 2.0.2
Show newest version
package com.autonomousapps.internal

import com.autonomousapps.internal.utils.buildDocument
import com.autonomousapps.internal.utils.mapToSet
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.File

internal class ManifestParser(
  /** The namespace, or package name, as set by the Android DSL. May be empty. */
  private val namespace: String
) {

  class ParseResult(
    val packageName: String,
    /** The value of the `android:name` attribute. May be empty. */
    val applicationName: String,
    /** The value of the `android:theme` attribute. May be empty. */
    val theme: String,
    val components: Map>
  )

  /**
   * The purpose of [allComponents] is to assist in the migration from the old to the new model.
   */
  @Throws(ManifestParseException::class)
  fun parse(manifest: File, allComponents: Boolean = false): ParseResult {
    val document = buildDocument(manifest)

    val packageName = packageName(manifest, document)
    val application = application(document)
    val applicationName = application?.getAttribute("android:name") ?: ""

    val theme = application?.attributes?.getNamedItem("android:theme")
      ?.nodeValue?.substringAfter("@style/").orEmpty()

    val services = application?.componentNames(Manifest.Component.SERVICE, packageName) ?: emptySet()
    val providers = application?.componentNames(Manifest.Component.PROVIDER, packageName) ?: emptySet()
    val activities = application?.componentNames(Manifest.Component.ACTIVITY, packageName) ?: emptySet()
    val receivers = application?.componentNames(Manifest.Component.RECEIVER, packageName) ?: emptySet()

    val componentsMapping = mutableMapOf>()

    // "service" is enough to catch LeakCanary, and "provider" makes sense in principle. Trying not to be too aggressive.
    if (services.isNotEmpty()) componentsMapping[Manifest.Component.SERVICE.mapKey] = services
    if (providers.isNotEmpty()) componentsMapping[Manifest.Component.PROVIDER.mapKey] = providers

    if (allComponents) {
      if (activities.isNotEmpty()) componentsMapping[Manifest.Component.ACTIVITY.mapKey] = activities
      if (receivers.isNotEmpty()) componentsMapping[Manifest.Component.RECEIVER.mapKey] = receivers
    }

    return ParseResult(
      packageName = packageName,
      applicationName = applicationName,
      theme = theme,
      components = componentsMapping
    )
  }

  private fun application(document: Document): Element? {
    val elements = document.getElementsByTagName("application")
    return if (elements.length > 0) {
      elements.item(0) as Element
    } else {
      null
    }
  }

  // https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/issues/700
  private fun packageName(manifest: File, document: Document): String {
    return namespace.ifEmpty {
      runCatching {
        document.getElementsByTagName("manifest").item(0)
          .attributes
          .getNamedItem("package")
          .nodeValue
      }.getOrElse { t ->
        throw if (t is NullPointerException) {
          ManifestParseException(
            "${manifest.path} has no 'package' attribute. You should use 'android.namespace' to set the package name " +
              "and remove the 'package' attribute from the main manifest, since that attribute is set for removal " +
              "with AGP 8.0.",
            t
          )
        } else {
          t
        }
      }
    }
  }

  private fun Element.componentNames(
    component: Manifest.Component,
    packageName: String
  ): Set {
    return getElementsByTagName(component.tagName)
      .mapToSet {
        it.attributes.getNamedItem(component.attrName).nodeValue.withPackageName(
          packageName
        )
      }
  }

  private fun String.withPackageName(packageName: String): String {
    return if (startsWith(".")) {
      // item name is relative, so prefix with the package name
      "$packageName$this"
    } else {
      // item name is absolute, so use it as-is
      this
    }
  }

  internal class ManifestParseException(
    msg: String, cause: Throwable
  ) : RuntimeException(msg, cause)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy