Skip to content

Instantly share code, notes, and snippets.

@exaland
Created September 30, 2022 12:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save exaland/1cede55dd4a377b667bb4edb773cef88 to your computer and use it in GitHub Desktop.
Save exaland/1cede55dd4a377b667bb4edb773cef88 to your computer and use it in GitHub Desktop.
Gradle Script for Android 12 Required Merge
import org.w3c.dom.Element
import org.w3c.dom.Node
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.TransformerFactory
import javax.xml.transform.Transformer
/**
* For apps targeting Android 12, if the AndroidManifest.xml file contains <activity>, <activity-alias>, <service>, or
* <receiver> components that contain <intent-filter>(s), it is required that those components explicitly declare the
* `android:exported` attribute (see https://developer.android.com/about/versions/12/behavior-changes-12#exported).
*
* This function automatically adds the missing `android:exported` attribute to components that require it. Prior to
* Android 12, for <activity>, <activity-alias>, <service> and <receiver> components that have <intent-filter>(s), if
* the `android:exported` attribute was not set explicitly, the default value would be `true`. The previous statement
* is based on researching documentation on the `android:exported` attribute:
* - https://developer.android.com/guide/topics/manifest/activity-element#exported
* - https://developer.android.com/guide/topics/manifest/activity-alias-element#exported
* - https://developer.android.com/guide/topics/manifest/service-element#exported
* - https://developer.android.com/guide/topics/manifest/receiver-element#exported
* Therefore, for <activity>, <activity-alias>, <service> and <receiver> components that have <intent-filter>(s), if
* the `android:exported` attribute is missing, this function adds the attribute with default value `true`.
* For known exceptions, set the value to `false`:
* - firebase messaging service: https://firebase.google.com/docs/cloud-messaging/android/client#manifest
*
* @param manifestFile the AndroidManifest.xml file to be investigated
*/
def addAndroidExportedIfNecessary(File manifestFile) {
def manifestAltered = false
def reader = manifestFile.newReader()
def document = groovy.xml.DOMBuilder.parse(reader)
def application = document.getElementsByTagName("application").item(0)
if (application != null) {
println "Searching for activities, services and receivers with intent filters..."
application.childNodes.each { child ->
def childNodeName = child.nodeName
if (childNodeName == "activity" || childNodeName == "activity-alias" ||
childNodeName == "service" || childNodeName == "receiver") {
def attributes = child.getAttributes()
if (attributes.getNamedItem("android:exported") == null) {
def intentFilters = child.childNodes.findAll {
it.nodeName == "intent-filter"
}
if (intentFilters.size() > 0) {
println "found ${childNodeName} ${attributes.getNamedItem("android:name").nodeValue} " +
"with intent filters but without android:exported attribute"
def exportedAttrAdded = false
for (def i = 0; i < intentFilters.size(); i++) {
def intentFilter = intentFilters[i]
def actions = intentFilter.childNodes.findAll {
it.nodeName == "action"
}
for (def j = 0; j < actions.size(); j++) {
def action = actions[j]
def actionName = action.getAttributes().getNamedItem("android:name").nodeValue
if (actionName == "com.google.firebase.MESSAGING_EVENT") {
println "adding exported=false to ${attributes.getNamedItem("android:name")}..."
((Element) child).setAttribute("android:exported", "false")
manifestAltered = true
exportedAttrAdded = true
}
}
}
if (!exportedAttrAdded) {
println "adding exported=true to ${attributes.getNamedItem("android:name")}..."
((Element) child).setAttribute("android:exported", "true")
manifestAltered = true
}
}
}
}
}
}
if (manifestAltered) {
document.setXmlStandalone(true)
Transformer transformer = TransformerFactory.newInstance().newTransformer()
DOMSource source = new DOMSource(document)
FileWriter writer = new FileWriter(manifestFile)
StreamResult result = new StreamResult(writer)
transformer.transform(source, result)
println "Done adding missing android:exported attributes to your AndroidManifest.xml. You may want to" +
"additionally prettify it in Android Studio using [command + option + L](mac) or [CTRL+ALT+L](windows)."
} else {
println "Hooray, your AndroidManifest.xml did not need any change."
}
}
/**
* Given an AndroidManifest.xml file, extract components with missing `android:exported` attribute, also add that
* attribute to those components.
*/
def getMissingAndroidExportedComponents(File manifestFile) {
List<Node> nodesFromDependencies = new ArrayList<>()
def reader = manifestFile.newReader()
def document = groovy.xml.DOMBuilder.parse(reader)
def application = document.getElementsByTagName("application").item(0)
if (application != null) {
println "Searching for activities, services and receivers with intent filters..."
application.childNodes.each { child ->
def childNodeName = child.nodeName
if (childNodeName == "activity" || childNodeName == "activity-alias" ||
childNodeName == "service" || childNodeName == "receiver") {
def attributes = child.getAttributes()
if (attributes.getNamedItem("android:exported") == null) {
def intentFilters = child.childNodes.findAll {
it.nodeName == "intent-filter"
}
if (intentFilters.size() > 0) {
println "found ${childNodeName} ${attributes.getNamedItem("android:name").nodeValue} " +
"with intent filters but without android:exported attribute"
def exportedAttrAdded = false
for (def i = 0; i < intentFilters.size(); i++) {
def intentFilter = intentFilters[i]
def actions = intentFilter.childNodes.findAll {
it.nodeName == "action"
}
for (def j = 0; j < actions.size(); j++) {
def action = actions[j]
def actionName = action.getAttributes().getNamedItem("android:name").nodeValue
if (actionName == "com.google.firebase.MESSAGING_EVENT") {
println "adding exported=false to ${attributes.getNamedItem("android:name")}..."
((Element) child).setAttribute("android:exported", "false")
exportedAttrAdded = true
}
}
}
if (!exportedAttrAdded) {
println "adding exported=true to ${attributes.getNamedItem("android:name")}..."
((Element) child).setAttribute("android:exported", "true")
}
nodesFromDependencies.add(child)
}
}
}
}
}
return nodesFromDependencies
}
/**
* Add [components] to the given an AndroidManifest.xml file's <application> component
*/
def addManifestFileComponents(File manifestFile, List<Node> components) {
def reader = manifestFile.newReader()
def document = groovy.xml.DOMBuilder.parse(reader)
def application = document.getElementsByTagName("application").item(0)
if (application != null) {
println "Adding missing components with android:exported attribute to ${manifestFile.absolutePath} ..."
components.each { node ->
Node importedNode = document.importNode(node, true)
application.appendChild(importedNode)
}
}
if (components.size() > 0) {
document.setXmlStandalone(true)
Transformer transformer = TransformerFactory.newInstance().newTransformer()
DOMSource source = new DOMSource(document)
FileWriter writer = new FileWriter(manifestFile)
StreamResult result = new StreamResult(writer)
transformer.transform(source, result)
println "Added missing app-dependencies components with android:exported attributes to your " +
"AndroidManifest.xml.You may want to additionally prettify it in Android Studio using " +
"[command + option + L](mac) or [CTRL+ALT+L](windows)."
}
println "----"
}
task doAddAndroidExportedIfNecessary {
doLast {
def root = new File(project.rootDir, "")
if (root.isDirectory()) {
def children = root.listFiles()
for (def i = 0; i < children.size(); i++) {
File child = children[i]
if (child.isDirectory()) {
File srcDirectory = new File(child, "src")
if (srcDirectory.exists() && srcDirectory.isDirectory()) {
def srcChildren = srcDirectory.listFiles()
for (def j = 0; j < srcChildren.size(); j++) {
File manifestFile = new File(srcChildren[j], "AndroidManifest.xml")
if (manifestFile.exists() && manifestFile.isFile()) {
println "found manifest file: ${manifestFile.absolutePath}"
addAndroidExportedIfNecessary(manifestFile)
println "-----"
}
}
}
}
}
}
}
}
/**
* If your project has dependency on libraries that haven't updated their AndroidManifest.xml files yet to conform to
* the Android 12 requirement, your app may still fail to build due to missing `android:exported` attributes in those
* libraries' AndroidManifest.xml files, even after running the [doAddAndroidExportedIfNecessary] task. This task
* extracts the components that are missing the `android:exported` attribute from the merged manifest, which includes
* components from imported libraries, then adds the components to the project's AndroidManifest.xml files that contains
* <application> component. The added components should override their declaration in the libraries' manifest files.
* As we cannot modify the libraries' manifest files, this should be an acceptable workaround.
*
* NOTE: always run [doAddAndroidExportedIfNecessary] first before running this task, in order to avoid adding duplicate
* components to the project's AndroidManifest.xml files. After [doAddAndroidExportedIfNecessary] finishes, rebuild your
* project, otherwise the merged manifest won't be created. Only after those steps, execute this task.
*
* NOTE: This task assumes certain structure of the path to the merged manifest, which is created after project
* build. The path structure may be dependent on the gradle version. This task was tested with gradle-6.8 and
* Android Studio Arctic Fox.
*
* NOTE: If your project already targets Android 12 and still contains libraries with missing `android:exported`
* attributes for required components in their AndroidManifest.xml files, your build will fail and the merged manifest
* won't be created. Therefore, call this task before you target Android 12; or:
* - temporarily downgrade the targetSdkVersion (and compileSDKVersion) to 30
* - run [doAddAndroidExportedIfNecessary] task
* - rebuild your project (to build the merged manifest)
* - run this task
* - set the targetSdkVersion back to target Android 12
*/
task doAddAndroidExportedForDependencies {
doLast {
List<Node> missingComponents = new ArrayList<>()
def root = new File(project.rootDir, "")
if (root.isDirectory()) {
def children = root.listFiles()
for (def i = 0; i < children.size(); i++) {
File child = children[i]
if (child.isDirectory()) {
File mergedManifestsDirectory = new File(child, "build/intermediates/merged_manifests")
if (mergedManifestsDirectory.exists() && mergedManifestsDirectory.isDirectory()) {
def manifestFiles = mergedManifestsDirectory.listFiles().findAll { directoryChild ->
directoryChild.isDirectory() &&
(new File(directoryChild, "AndroidManifest.xml")).exists()
}.stream().map { directoryWithManifest ->
new File(directoryWithManifest, "AndroidManifest.xml")
}.toArray()
if (manifestFiles.size() > 0) {
File mergedManifest = manifestFiles[0]
if (mergedManifest.exists() && mergedManifest.isFile()) {
missingComponents = getMissingAndroidExportedComponents(mergedManifest)
if (missingComponents.size() > 0) {
File srcDirectory = new File(child, "src")
if (srcDirectory.exists() && srcDirectory.isDirectory()) {
def srcChildren = srcDirectory.listFiles()
for (def j = 0; j < srcChildren.size(); j++) {
File manifestFile = new File(srcChildren[j], "AndroidManifest.xml")
if (manifestFile.exists() && manifestFile.isFile()) {
addManifestFileComponents(manifestFile, missingComponents)
}
}
}
}
}
}
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment