Skip to content

Commit

Permalink
Remove the manifest hack (#399)
Browse files Browse the repository at this point in the history
* Remove the manifest hack.

* Uncomment the CropImageContractTest
  • Loading branch information
PaulWoitaschek committed Jun 29, 2022
1 parent b8ae101 commit 2f867e9
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 531 deletions.
290 changes: 0 additions & 290 deletions build.gradle
Expand Up @@ -24,293 +24,3 @@ allprojects {
maven { url "https://jitpack.io" }
}
}


/**
* 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 file contains gradle task for adding missing `android:exported` attributes to AndroidManifest.xml files.
*
* 1. copy the content of this file to your `build.gradle` file located in your project's root folder.
* 2. in terminal (cmd), in your project's root folder, execute `./gradlew doAddAndroidExportedIfNecessary`
* 3. if your project still fails to build with the same error about missing the `android:exported` attribute,
* check out the [doAddAndroidExportedForDependencies] task
*
* DISCLAIMER: the gradle task is to help you avoid manually adding the `android:exported` attribute. This comes in
* handy for projects with large AndroidManifest.xml file, projects with multiple AndroidManifest.xml files due to
* multiple flavors and/or multiple app modules. At the end, you should always review changes done on your files
* by this gradle task. Check the following links to make sure the added `android:exported` attributes match your
* use cases:
* - 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
*
*/

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)
}
}
}
}
}
}
}
}
}
}
}
}
5 changes: 4 additions & 1 deletion cropper/build.gradle
Expand Up @@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'maven-publish'

group='com.canhub.cropper'
group = 'com.canhub.cropper'

// Because the components are created only during the afterEvaluate phase, you must
// configure your publications using the afterEvaluate() lifecycle method.
Expand Down Expand Up @@ -69,4 +69,7 @@ dependencies {
testImplementation "androidx.test:core:$androidXTestVersion"
testImplementation "androidx.test:runner:$androidXTestVersion"
testImplementation "io.mockk:mockk:$mockkVersion"

testImplementation "androidx.fragment:fragment-testing:$androidXFragmentTestingVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
}
26 changes: 0 additions & 26 deletions cropper/src/main/AndroidManifest.xml
Expand Up @@ -24,31 +24,5 @@
<activity
android:name=".CropImageActivity"
android:exported="true" />

<!-- This is here because the library did not update yet to Android 12 so we force the exported value when merging manifests -->
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity"
android:exported="true"
android:theme="@android:style/Theme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity"
android:exported="true"
android:theme="@android:style/Theme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyFloatingActivity"
android:exported="true"
android:theme="@android:style/Theme.Dialog">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
</application>
</manifest>

0 comments on commit 2f867e9

Please sign in to comment.