Created
September 30, 2022 12:24
-
-
Save exaland/1cede55dd4a377b667bb4edb773cef88 to your computer and use it in GitHub Desktop.
Gradle Script for Android 12 Required Merge
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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