diff --git a/projects/sdk/core/gradle-plugin/build.gradle b/projects/sdk/core/gradle-plugin/build.gradle index b3f479cd7..1339d6feb 100644 --- a/projects/sdk/core/gradle-plugin/build.gradle +++ b/projects/sdk/core/gradle-plugin/build.gradle @@ -16,6 +16,7 @@ dependencies { implementation "com.android.tools.build:gradle:$build_gradle_version" implementation 'com.googlecode.json-simple:json-simple:1.1' implementation project(':transform') + implementation project(':manifest-parser') testImplementation 'junit:junit:4.12' testImplementation gradleTestKit() diff --git a/projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/ShadowPlugin.kt b/projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/ShadowPlugin.kt index 665fb4ba4..f568c4999 100644 --- a/projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/ShadowPlugin.kt +++ b/projects/sdk/core/gradle-plugin/src/main/kotlin/com/tencent/shadow/core/gradle/ShadowPlugin.kt @@ -18,14 +18,18 @@ package com.tencent.shadow.core.gradle +import com.android.build.gradle.AppExtension import com.android.build.gradle.AppPlugin import com.android.build.gradle.BaseExtension +import com.android.build.gradle.tasks.ProcessMultiApkApplicationManifest import com.tencent.shadow.core.gradle.extensions.PackagePluginExtension +import com.tencent.shadow.core.manifest_parser.generatePluginManifest import com.tencent.shadow.core.transform.ShadowTransform import com.tencent.shadow.core.transform_kit.AndroidClassPoolBuilder import com.tencent.shadow.core.transform_kit.ClassPoolBuilder import org.gradle.api.* import org.gradle.api.plugins.BasePlugin +import org.gradle.api.provider.Property import java.io.File import kotlin.reflect.full.declaredFunctions import kotlin.reflect.jvm.isAccessible @@ -61,6 +65,8 @@ class ShadowPlugin : Plugin { initAndroidClassPoolBuilder(baseExtension, project) createPackagePluginTasks(project) + + createGeneratePluginManifestTasks(project) } } @@ -83,6 +89,42 @@ class ShadowPlugin : Plugin { } } + private fun createGeneratePluginManifestTasks(project: Project) { + val appExtension: AppExtension = project.extensions.getByType(AppExtension::class.java) + appExtension.applicationVariants.filter { variant -> + variant.productFlavors.any { flavor -> + flavor.dimension == ShadowTransform.DimensionName && + flavor.name == ShadowTransform.ApplyShadowTransformFlavorName + } + }.forEach { pluginVariant -> + val output = pluginVariant.outputs.first() + val processManifestTask = output.processManifestProvider.get() + val manifestFile = (processManifestTask as ProcessMultiApkApplicationManifest).mainMergedManifest.get().asFile + val variantName = manifestFile.parentFile.name + val outputDir = File(project.buildDir, "generated/source/pluginManifest/$variantName") + + // 添加生成PluginManifest.java任务 + val task = project.tasks.register("generate${variantName.capitalize()}PluginManifest") { + it.dependsOn(processManifestTask) + it.inputs.file(manifestFile) + it.outputs.dir(outputDir).withPropertyName("outputDir") + val packageForR = (project.tasks.getByName("processPluginDebugResources").property("namespace") as Property).get() + it.doLast { + generatePluginManifest(manifestFile, + outputDir, + "com.tencent.shadow.core.manifest_parser", + packageForR) + } + } + project.tasks.getByName("compile${variantName.capitalize()}JavaWithJavac").dependsOn(task) + + // 把PluginManifest.java添加为源码 + appExtension.sourceSets.getByName(variantName).java { + srcDir(outputDir) + } + } + } + private fun addFlavorForTransform(baseExtension: BaseExtension) { baseExtension.flavorDimensionList.add(ShadowTransform.DimensionName) try { diff --git a/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageMultiPluginTest.kt b/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageMultiPluginTest.kt index b3c22bda4..fe962d9a5 100644 --- a/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageMultiPluginTest.kt +++ b/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageMultiPluginTest.kt @@ -47,6 +47,7 @@ class PackageMultiPluginTest { .withProjectDir(ROOT_PROJECT_DIR) .withPluginClasspath() .withArguments(listOf( + "-xgeneratePluginDebugPluginManifest", "-Pdisable_shadow_transform=true", ":plugin1:PackageMultiPlugin" )) diff --git a/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageOnlyPluginTest.kt b/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageOnlyPluginTest.kt index 76f247cc5..750d6d72f 100644 --- a/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageOnlyPluginTest.kt +++ b/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackageOnlyPluginTest.kt @@ -46,6 +46,7 @@ class PackageOnlyPluginTest { .withProjectDir(PLUGIN1_PROJECT_DIR) .withPluginClasspath() .withArguments(listOf( + "-xgeneratePluginDebugPluginManifest", "-Pdisable_shadow_transform=true", ":plugin1:packageOnlyApkPlugin" )) diff --git a/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackagePluginTaskTest.kt b/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackagePluginTaskTest.kt index ffb80c1d0..cd3e7cf2c 100644 --- a/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackagePluginTaskTest.kt +++ b/projects/sdk/core/gradle-plugin/src/test/kotlin/com/tencent/shadow/core/gradle/PackagePluginTaskTest.kt @@ -48,6 +48,7 @@ class PackagePluginTaskTest { .withProjectDir(ROOT_PROJECT_DIR) .withPluginClasspath() .withArguments(listOf( + "-xgeneratePluginDebugPluginManifest", "-Pdisable_shadow_transform=true", ":plugin1:packageDebugPlugin" )) diff --git a/projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/classloaders/PluginClassLoader.kt b/projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/classloaders/PluginClassLoader.kt index ea0e76948..4352b5b2f 100644 --- a/projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/classloaders/PluginClassLoader.kt +++ b/projects/sdk/core/loader/src/main/kotlin/com/tencent/shadow/core/loader/classloaders/PluginClassLoader.kt @@ -19,6 +19,7 @@ package com.tencent.shadow.core.loader.classloaders import android.os.Build +import com.tencent.shadow.core.runtime.PluginManifest import dalvik.system.BaseDexClassLoader import org.jetbrains.annotations.TestOnly import java.io.File @@ -111,6 +112,11 @@ class PluginClassLoader( return clazz } + internal fun loadPluginManifest(): PluginManifest { + val clazz = findClass("com.tencent.shadow.core.manifest_parser.PluginManifest") + return PluginManifest::class.java.cast(clazz.newInstance()) + } + } private fun String.subStringBeforeDot() = substringBeforeLast('.', "") diff --git a/projects/sdk/core/manifest-parser/.gitignore b/projects/sdk/core/manifest-parser/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/projects/sdk/core/manifest-parser/.gitignore @@ -0,0 +1 @@ +/build diff --git a/projects/sdk/core/manifest-parser/build.gradle b/projects/sdk/core/manifest-parser/build.gradle new file mode 100644 index 000000000..88d8ae8d0 --- /dev/null +++ b/projects/sdk/core/manifest-parser/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'kotlin' + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.squareup:javapoet:1.11.1' + implementation project(':runtime') + testImplementation 'junit:junit:4.12' + testImplementation 'commons-io:commons-io:2.9.0' + testImplementation 'com.tencent.shadow.coding:java-build-config' +} + +compileKotlin { + kotlinOptions { + jvmTarget = "1.6" + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.6" + } +} diff --git a/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestKeys.kt b/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestKeys.kt new file mode 100644 index 000000000..3a2e2ad96 --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestKeys.kt @@ -0,0 +1,25 @@ +package com.tencent.shadow.core.manifest_parser + +sealed class AndroidManifestKeys { + companion object { + const val appComponentFactory = "android:appComponentFactory" + const val `package` = "package" + const val name = "android:name" + const val theme = "android:theme" + const val configChanges = "android:configChanges" + const val windowSoftInputMode = "android:windowSoftInputMode" + const val authorities = "android:authorities" + const val `intent-filter` = "intent-filter" + const val action = "action" + const val manifest = "manifest" + const val application = "application" + const val activity = "activity" + const val service = "service" + const val provider = "provider" + const val receiver = "receiver" + } +} +typealias ComponentMapKey = String +typealias ComponentMapValue = Any +typealias ComponentMap = Map +typealias MutableComponentMap = MutableMap \ No newline at end of file diff --git a/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestReader.kt b/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestReader.kt new file mode 100644 index 000000000..86280b9a7 --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestReader.kt @@ -0,0 +1,177 @@ +package com.tencent.shadow.core.manifest_parser + +import org.w3c.dom.Document +import org.w3c.dom.Element +import org.w3c.dom.Node +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory + +typealias ManifestMap = Map + +/** + * 读取xml格式的Manifest到内存Map中 + */ +class AndroidManifestReader { + /** + * 读取入口方法 + * + * @param xmlFile com.android.build.gradle.tasks.ManifestProcessorTask任务的输出文件, + * 一般位于apk工程的build/intermediates/merged_manifest目录中。 + */ + fun read(xmlFile: File): ManifestMap { + val manifest = readXml(xmlFile).documentElement + val application = readApplication(manifest) + val globalAttributes = readGlobalAttributes(manifest, application) + val components = readComponents(application) + return globalAttributes.plus(components) + } + + private fun readXml(xmlFile: File): Document { + try { + val documentBuilderFactory = DocumentBuilderFactory.newDefaultInstance() + val documentBuilder = documentBuilderFactory.newDocumentBuilder() + return documentBuilder.parse(xmlFile)!! + } catch (e: Exception) { + throw RuntimeException("xml应该是AGP生成的合法文件,所以不兼容任何xml读取错误", e) + } + } + + private fun readApplication(manifest: Element): Element? { + val elements = manifest.getElementsByTagName(AndroidManifestKeys.application) + return if (elements.length == 1) { + val node = elements.item(0) + assert(node.nodeType == Node.ELEMENT_NODE) + elements.item(0) as Element + } else { + null + } + } + + /** + * 读取Manifest中那些唯一的属性 + */ + private fun readGlobalAttributes(manifest: Element, application: Element?): Map { + val globalAttributes = mutableMapOf() + + fun manifestAttribute(name: String) { + globalAttributes[name] = manifest.getAttribute(name) + } + + fun applicationAttribute(name: String) { + if (application != null) { + val attribute = application.getAttribute(name) + if (attribute.isNotEmpty()) { + globalAttributes[name] = attribute + } + } + } + + manifestAttribute(AndroidManifestKeys.`package`) + listOf( + AndroidManifestKeys.name, + AndroidManifestKeys.theme, + AndroidManifestKeys.appComponentFactory, + ).forEach(::applicationAttribute) + return globalAttributes + } + + private fun readComponents(application: Element?) = + listOf( + AndroidManifestKeys.activity to ::parseActivity, + AndroidManifestKeys.service to ::parseService, + AndroidManifestKeys.receiver to ::parseReceiver, + AndroidManifestKeys.provider to ::parseProvider, + ).map { (componentKey, parseMethod) -> + val componentArray = parseComponents(application, componentKey, parseMethod) + componentKey to componentArray + } + + + private fun parseComponents( + application: Element?, + componentKey: String, + parseFunction: (Element) -> ComponentMap + ): Array { + if (application == null) { + return emptyArray() + } + val nodeList = application.getElementsByTagName(componentKey) + val length = nodeList.length + val collectionList = mutableListOf() + for (i in 0 until length) { + val node = nodeList.item(i) + assert(node.nodeType == Node.ELEMENT_NODE) + val map = parseFunction(node as Element) + collectionList.add(map) + } + return collectionList.toTypedArray() + } + + private fun parseActivity(element: Element): ComponentMap { + val activityMap = parseComponent(element).toMutableMap() + + listOf( + AndroidManifestKeys.theme, + AndroidManifestKeys.configChanges, + AndroidManifestKeys.windowSoftInputMode, + ).forEach { attributeKey -> + activityMap.putAttributeIfNotNull(element, attributeKey) + } + return activityMap + } + + private fun parseService(element: Element): ComponentMap { + return parseComponent(element) + } + + private fun parseReceiver(element: Element): ComponentMap { + val receiverMap = parseComponent(element).toMutableMap() + + val receiverActions = parseReceiverActions(element) + if (receiverActions.isNotEmpty()) { + receiverMap[AndroidManifestKeys.action] = receiverActions + } + + return receiverMap + } + + private fun parseReceiverActions(receiverElement: Element): List { + val intentFilters = receiverElement.getElementsByTagName(AndroidManifestKeys.`intent-filter`) + val collectionList = mutableListOf() + for (i in 0 until intentFilters.length) { + val intentFilter = intentFilters.item(i) + assert(intentFilter.nodeType == Node.ELEMENT_NODE) + val actions = (intentFilter as Element).getElementsByTagName(AndroidManifestKeys.action) + for (j in 0 until actions.length) { + val action = actions.item(j) + assert(action.nodeType == Node.ELEMENT_NODE) + val actionName = (action as Element).getAttribute(AndroidManifestKeys.name) + collectionList.add(actionName) + } + } + return collectionList + } + + private fun parseProvider(element: Element): ComponentMap { + val providerMap = parseComponent(element).toMutableMap() + + providerMap.putAttributeIfNotNull(element, AndroidManifestKeys.authorities) + + return providerMap + } + + private fun parseComponent(element: Element): ComponentMap { + val componentName = element.getAttribute(AndroidManifestKeys.name) + return mapOf( + AndroidManifestKeys.name to componentName + ) + } + + private fun MutableComponentMap.putAttributeIfNotNull( + componentElement: Element, + attributeKey: String) { + if (componentElement.hasAttribute(attributeKey)) { + this[attributeKey] = componentElement.getAttribute(attributeKey) + } + } +} diff --git a/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/ManifestParser.kt b/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/ManifestParser.kt new file mode 100644 index 000000000..584a1952f --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/ManifestParser.kt @@ -0,0 +1,18 @@ +package com.tencent.shadow.core.manifest_parser + +import java.io.File + +/** + * manifest-parser的入口方法 + * + * @param xmlFile com.android.build.gradle.tasks.ManifestProcessorTask任务的输出文件, + * 一般位于apk工程的build/intermediates/merged_manifest目录中。 + * @param outputDir 生成文件的输出目录 + * @param packageName 生成类的包名 + * @param packageForR 生成对R.java引用时需要的R文件的包名 + */ +fun generatePluginManifest(xmlFile: File, outputDir: File, packageName: String, packageForR: String) { + val androidManifest = AndroidManifestReader().read(xmlFile) + val generator = PluginManifestGenerator(packageForR) + generator.generate(androidManifest, outputDir, packageName) +} \ No newline at end of file diff --git a/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/PluginManifestGenerator.kt b/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/PluginManifestGenerator.kt new file mode 100644 index 000000000..1c6a93b3a --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/main/kotlin/com/tencent/shadow/core/manifest_parser/PluginManifestGenerator.kt @@ -0,0 +1,361 @@ +package com.tencent.shadow.core.manifest_parser + +import com.squareup.javapoet.* +import com.tencent.shadow.core.runtime.PluginManifest +import java.io.File +import java.util.* +import javax.lang.model.element.Modifier + +/** + * PluginManifest.java生成器 + * + * 将Loader所需的插件Manifest信息生成为Java文件, + * 添加runtime中PluginManifest接口的实现方法 + * + * @param packageForR 生成对R.java引用时需要的R文件的包名 + */ +class PluginManifestGenerator(private val packageForR: String) { + /** + * 生成器入口方法 + * + * 根据AndroidManifestReader输出的Map生成PluginManifest.java到outputDir目录中。 + * + * @param manifestMap AndroidManifestReader#read的输出Map + * @param outputDir 生成文件的输出目录 + * @param packageName 生成类的包名 + */ + fun generate(manifestMap: ManifestMap, outputDir: File, packageName: String) { + val pluginManifestBuilder = PluginManifestBuilder(manifestMap, packageForR) + val pluginManifest = pluginManifestBuilder.build() + JavaFile.builder(packageName, pluginManifest) + .build() + .writeTo(outputDir) + } +} + +private class PluginManifestBuilder( + val manifestMap: ManifestMap, + val packageForR: String +) { + val classBuilder: TypeSpec.Builder = + TypeSpec.classBuilder("PluginManifest") + .addSuperinterface(ClassName.get(PluginManifest::class.java)) + .addModifiers(Modifier.PUBLIC)!! + + fun build(): TypeSpec { + listOf( + *buildApplicationFields(), + buildActivityInfoArrayField(), + buildServiceInfoArrayField(), + buildReceiverInfoArrayField(), + buildProviderInfoArrayField(), + ).forEach { fieldSpec -> + val getterMethod = buildGetterMethod(fieldSpec) + classBuilder.addField(fieldSpec) + classBuilder.addMethod(getterMethod) + } + return classBuilder.build() + } + + private fun buildApplicationFields(): Array { + val stringFields = mapOf( + "applicationPackageName" to AndroidManifestKeys.`package`, + "applicationClassName" to AndroidManifestKeys.name, + "appComponentFactory" to AndroidManifestKeys.appComponentFactory, + ).map { (fieldName, key) -> + buildStringField(fieldName, key) + } + + val resIdFields = mapOf( + "applicationTheme" to AndroidManifestKeys.theme, + ).map { (fieldName, key) -> + buildResIdField(fieldName, key) + } + + return (stringFields + resIdFields).toTypedArray() + } + + private fun buildActivityInfoArrayField() = buildComponentArrayField( + AndroidManifestKeys.activity, + "ActivityInfo", + "activities", + ::toNewActivityInfo, + ) + + private fun buildServiceInfoArrayField() = buildComponentArrayField( + AndroidManifestKeys.service, + "ServiceInfo", + "services", + ::toNewServiceInfo, + ) + + private fun buildReceiverInfoArrayField() = buildComponentArrayField( + AndroidManifestKeys.receiver, + "ReceiverInfo", + "receivers", + ::toNewReceiverInfo, + ) + + private fun buildProviderInfoArrayField() = buildComponentArrayField( + AndroidManifestKeys.provider, + "ProviderInfo", + "providers", + ::toNewProviderInfo, + ) + + private fun buildComponentArrayField( + key: String, + subClassName: String, + fieldName: String, + transform: (ComponentMap) -> String + ): FieldSpec { + @Suppress("UNCHECKED_CAST") + val componentMapArray = manifestMap[key] as Array + val literal = componentMapArray.joinToString( + separator = ",\n", + prefix = "{\n", + postfix = "\n}", + transform = transform + ) + + val componentInfoArrayTypeName = ArrayTypeName.of(ClassName.get( + "com.tencent.shadow.core.runtime", + "PluginManifest", + subClassName)) + + val codeBlock = if (componentMapArray.isNotEmpty()) { + CodeBlock.of("new \$1T \$2L", componentInfoArrayTypeName, literal) + } else { + nullCodeBlock() + } + return privateStaticFinalFieldBuilder( + componentInfoArrayTypeName, + fieldName, + ).initializer( + codeBlock + ).build() + } + + private fun buildStringField(fieldName: String, key: String): FieldSpec { + val value = manifestMap[key] + val codeBlock = if (value != null) { + CodeBlock.of("\"$1L\"", value) + } else { + nullCodeBlock() + } + return privateStaticFinalStringFieldBuilder(fieldName) + .initializer(codeBlock).build() + } + + private fun buildGetterMethod(fieldSpec: FieldSpec): MethodSpec = + MethodSpec.methodBuilder("get${fieldSpec.name.capitalize()}") + .addModifiers( + Modifier.PUBLIC, + Modifier.FINAL, + ) + .returns(fieldSpec.type) + .addStatement(CodeBlock.of("return ${fieldSpec.name}")) + .build() + + private fun buildResIdField(fieldName: String, key: String): FieldSpec { + val manifestValue = manifestMap[key] + return if (manifestValue != null) { + buildResIdFieldWithValue(fieldName, manifestValue) + } else { + privateStaticFinalIntFieldBuilder(fieldName) + .initializer( + CodeBlock.of("$1L", "0") + ).build() + } + } + + private fun buildResIdFieldWithValue( + fieldName: String, + manifestValue: Any, + ): FieldSpec { + + val resIdLiteral = themeStringToResId(manifestValue, packageForR) + return privateStaticFinalIntFieldBuilder(fieldName) + .initializer( + CodeBlock.of("$1L", resIdLiteral) + ).build() + } + + + private fun toNewActivityInfo(componentMap: ComponentMap): String { + fun makeResIdLiteral(key: String, + valueToResId: (value: String) -> String): String { + val value = componentMap[key] as String? + val literal = if (value != null) { + valueToResId(value) + } else { + "0" + } + return literal + } + + val themeLiteral = makeResIdLiteral(AndroidManifestKeys.theme) { + themeStringToResId(it, packageForR) + } + val configChangesLiteral = makeResIdLiteral(AndroidManifestKeys.configChanges) { + configChangesToInt(it) + } + val softInputModeLiteral = makeResIdLiteral(AndroidManifestKeys.windowSoftInputMode) { + windowSoftInputModeToInt(it) + } + + return "new com.tencent.shadow.core.runtime.PluginManifest" + + ".ActivityInfo(" + + "\"${componentMap[AndroidManifestKeys.name]}\", " + + "$themeLiteral ," + + "$configChangesLiteral ," + + softInputModeLiteral + + ")" + } + + private fun toNewServiceInfo(componentMap: ComponentMap): String { + return "new com.tencent.shadow.core.runtime.PluginManifest" + + ".ServiceInfo(\"${componentMap[AndroidManifestKeys.name]}\")" + } + + private fun toNewReceiverInfo(componentMap: ComponentMap): String { + @Suppress("UNCHECKED_CAST") + val actions = componentMap[AndroidManifestKeys.action] as List? + val actionsLiteral = + actions?.joinToString( + prefix = "new String[]{\"", + separator = "\", \"", + postfix = "\"}" + ) ?: "null" + + return "new com.tencent.shadow.core.runtime.PluginManifest" + + ".ReceiverInfo(\"${componentMap[AndroidManifestKeys.name]}\", " + + actionsLiteral + + ")" + } + + private fun toNewProviderInfo(componentMap: ComponentMap): String { + val authoritiesValue = componentMap[AndroidManifestKeys.authorities] + val authoritiesLiteral = + if (authoritiesValue != null) { + "\"${authoritiesValue}\"" + } else { + "null" + } + + return "new com.tencent.shadow.core.runtime.PluginManifest" + + ".ProviderInfo(\"${componentMap[AndroidManifestKeys.name]}\", $authoritiesLiteral)" + } + + companion object { + fun privateStaticFinalFieldBuilder(type: TypeName, fieldName: String) = FieldSpec.builder( + type, + fieldName, + Modifier.PRIVATE, + Modifier.STATIC, + Modifier.FINAL, + )!! + + fun privateStaticFinalStringFieldBuilder(fieldName: String) = + privateStaticFinalFieldBuilder( + ClassName.get(String::class.java), + fieldName, + ) + + fun privateStaticFinalIntFieldBuilder(fieldName: String) = + privateStaticFinalFieldBuilder( + TypeName.INT, + fieldName, + ) + + fun nullCodeBlock() = CodeBlock.of("null")!! + + fun themeStringToResId(manifestValue: Any, packageForR: String): String { + val resName = manifestValue as String // for example: @style/TestPluginTheme + val split = resName.split('/') + val packagePart = split[0] // for example: @style + val namePart = split[1] // for example: TestPluginTheme + + val isSystemResource = packagePart.startsWith("@android:") + val packageNameOfRClass = if (isSystemResource) { + "android" + } else { + packageForR + } + + val innerClassName = if (isSystemResource) { // for example: style + packagePart.removePrefix("@android:") + } else { + packagePart.removePrefix("@") + } + + val resIdFieldName = namePart.replace('.', '_') + return "${packageNameOfRClass}.R.${innerClassName}.${resIdFieldName}" + } + + private fun flagsToInt( + stringValue: String, + className: String, + fieldMap: (String) -> String, + ): String = + stringValue.split('|') + .map(String::trim) + .map(fieldMap) + .map { "${className}.${it}" } + .reduce { acc, i -> "$acc|$i" } + + private fun configChangesToInt(configChangesValue: String): String = + flagsToInt( + configChangesValue, + "android.content.pm.ActivityInfo" + ) { + when (it) { + "mcc" -> "CONFIG_MCC" + "mnc" -> "CONFIG_MNC" + "locale" -> "CONFIG_LOCALE" + "touchscreen" -> "CONFIG_TOUCHSCREEN" + "keyboard" -> "CONFIG_KEYBOARD" + "keyboardHidden" -> "CONFIG_KEYBOARD_HIDDEN" + "navigation" -> "CONFIG_NAVIGATION" + "orientation" -> "CONFIG_ORIENTATION" + "screenLayout" -> "CONFIG_SCREEN_LAYOUT" + "uiMode" -> "CONFIG_UI_MODE" + "screenSize" -> "CONFIG_SCREEN_SIZE" + "smallestScreenSize" -> "CONFIG_SMALLEST_SCREEN_SIZE" + "density" -> "CONFIG_DENSITY" + "layoutDirection" -> "CONFIG_LAYOUT_DIRECTION" + "colorMode" -> "CONFIG_COLOR_MODE" + "assetsPaths" -> "CONFIG_ASSETS_PATHS" + "fontScale" -> "CONFIG_FONT_SCALE" + "windowConfiguration" -> "CONFIG_WINDOW_CONFIGURATION" + else -> throw IllegalArgumentException("不认识$it") + } + } + + private fun windowSoftInputModeToInt(windowSoftInputModeValue: String): String = + flagsToInt( + windowSoftInputModeValue, + "android.view.WindowManager.LayoutParams" + ) { + when (it) { + "stateUnspecified" -> "SOFT_INPUT_STATE_UNSPECIFIED" + "stateUnchanged" -> "SOFT_INPUT_STATE_UNCHANGED" + "stateHidden" -> "SOFT_INPUT_STATE_HIDDEN" + "stateAlwaysHidden" -> "SOFT_INPUT_STATE_ALWAYS_HIDDEN" + "stateVisible" -> "SOFT_INPUT_STATE_VISIBLE" + "stateAlwaysVisible" -> "SOFT_INPUT_STATE_ALWAYS_VISIBLE" + "adjustUnspecified" -> "SOFT_INPUT_ADJUST_UNSPECIFIED" + "adjustResize" -> "SOFT_INPUT_ADJUST_RESIZE" + "adjustPan" -> "SOFT_INPUT_ADJUST_PAN" + "adjustNothing" -> "SOFT_INPUT_ADJUST_NOTHING" + "isForwardNavigation" -> "SOFT_INPUT_IS_FORWARD_NAVIGATION" + else -> throw IllegalArgumentException("不认识$it") + } + } + + private fun String.capitalize() = + replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + } +} diff --git a/projects/sdk/core/manifest-parser/src/test/java/android/R.java b/projects/sdk/core/manifest-parser/src/test/java/android/R.java new file mode 100644 index 000000000..71954adcf --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/java/android/R.java @@ -0,0 +1,7 @@ +package android; + +public final class R { + public static final class style { + public static final int Theme_NoTitleBar = 0x01030006; + } +} diff --git a/projects/sdk/core/manifest-parser/src/test/java/android/content/pm/ActivityInfo.java b/projects/sdk/core/manifest-parser/src/test/java/android/content/pm/ActivityInfo.java new file mode 100644 index 000000000..feade0592 --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/java/android/content/pm/ActivityInfo.java @@ -0,0 +1,42 @@ +package android.content.pm; + +/** + * mock for test + */ +public interface ActivityInfo { + int CONFIG_MCC = 0x0001; + + int CONFIG_MNC = 0x0002; + + int CONFIG_LOCALE = 0x0004; + + int CONFIG_TOUCHSCREEN = 0x0008; + + int CONFIG_KEYBOARD = 0x0010; + + int CONFIG_KEYBOARD_HIDDEN = 0x0020; + + int CONFIG_NAVIGATION = 0x0040; + + int CONFIG_ORIENTATION = 0x0080; + + int CONFIG_SCREEN_LAYOUT = 0x0100; + + int CONFIG_UI_MODE = 0x0200; + + int CONFIG_SCREEN_SIZE = 0x0400; + + int CONFIG_SMALLEST_SCREEN_SIZE = 0x0800; + + int CONFIG_DENSITY = 0x1000; + + int CONFIG_LAYOUT_DIRECTION = 0x2000; + + int CONFIG_COLOR_MODE = 0x4000; + + int CONFIG_ASSETS_PATHS = 0x80000000; + + int CONFIG_FONT_SCALE = 0x40000000; + + int CONFIG_WINDOW_CONFIGURATION = 0x20000000; +} diff --git a/projects/sdk/core/manifest-parser/src/test/java/android/view/WindowManager.java b/projects/sdk/core/manifest-parser/src/test/java/android/view/WindowManager.java new file mode 100644 index 000000000..3e5686e33 --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/java/android/view/WindowManager.java @@ -0,0 +1,21 @@ +package android.view; + +/** + * mock for test + */ +public interface WindowManager { + interface LayoutParams { + int SOFT_INPUT_STATE_UNSPECIFIED = 0; + int SOFT_INPUT_STATE_UNCHANGED = 1; + int SOFT_INPUT_STATE_HIDDEN = 2; + int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3; + int SOFT_INPUT_STATE_VISIBLE = 4; + int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5; + int SOFT_INPUT_MASK_ADJUST = 0xf0; + int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00; + int SOFT_INPUT_ADJUST_RESIZE = 0x10; + int SOFT_INPUT_ADJUST_PAN = 0x20; + int SOFT_INPUT_ADJUST_NOTHING = 0x30; + int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100; + } +} diff --git a/projects/sdk/core/manifest-parser/src/test/java/com/tencent/shadow/sample/plugin/app/lib/R.java b/projects/sdk/core/manifest-parser/src/test/java/com/tencent/shadow/sample/plugin/app/lib/R.java new file mode 100644 index 000000000..9780277ca --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/java/com/tencent/shadow/sample/plugin/app/lib/R.java @@ -0,0 +1,7 @@ +package com.tencent.shadow.sample.plugin.app.lib; + +public final class R { + public static final class style { + public static final int TestPluginTheme = 0x7f030001; + } +} diff --git a/projects/sdk/core/manifest-parser/src/test/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestReaderTest.kt b/projects/sdk/core/manifest-parser/src/test/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestReaderTest.kt new file mode 100644 index 000000000..6c87e61f5 --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/kotlin/com/tencent/shadow/core/manifest_parser/AndroidManifestReaderTest.kt @@ -0,0 +1,17 @@ +package com.tencent.shadow.core.manifest_parser + +import org.junit.Assert +import org.junit.Test +import java.io.File + +class AndroidManifestReaderTest { + @Test + fun testReadXml() { + val testFile = File(javaClass.classLoader.getResource("sample-app.xml")!!.toURI()) + val androidManifest = AndroidManifestReader().read(testFile) + Assert.assertEquals("com.tencent.shadow.sample.host", androidManifest[AndroidManifestKeys.`package`]) + Assert.assertEquals("com.tencent.shadow.sample.plugin.app.lib.UseCaseApplication", androidManifest[AndroidManifestKeys.name]) + Assert.assertEquals("com.tencent.shadow.test.plugin.androidx_cases.lib.TestComponentFactory", androidManifest[AndroidManifestKeys.appComponentFactory]) + Assert.assertEquals("@android:style/Theme.NoTitleBar", androidManifest[AndroidManifestKeys.theme]) + } +} \ No newline at end of file diff --git a/projects/sdk/core/manifest-parser/src/test/kotlin/com/tencent/shadow/core/manifest_parser/PluginManifestGeneratorTest.kt b/projects/sdk/core/manifest-parser/src/test/kotlin/com/tencent/shadow/core/manifest_parser/PluginManifestGeneratorTest.kt new file mode 100644 index 000000000..f1795e39c --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/kotlin/com/tencent/shadow/core/manifest_parser/PluginManifestGeneratorTest.kt @@ -0,0 +1,40 @@ +package com.tencent.shadow.core.manifest_parser + +import org.junit.Assert +import org.junit.Test +import java.io.File + +class PluginManifestGeneratorTest { + + @Test + fun testCompileCaseAsLittleAsPossible() { + testCompile("case_as_little_as_possible.xml", "") + } + + @Test + fun testNoAppComponentFactory() { + testCompile("noAppComponentFactory.xml", "") + } + + @Test + fun testCompileSampleApp() { + testCompile("sample-app.xml", "com.tencent.shadow.sample.plugin.app.lib") + } + + private fun testCompile(case: String, packageForR: String) { + val testFile = File(javaClass.classLoader.getResource(case)!!.toURI()) + val androidManifest = AndroidManifestReader().read(testFile) + val generator = PluginManifestGenerator(packageForR) + + val tempBuildDir = File("build", "PluginManifestGeneratorTest") + val outputDir = File(tempBuildDir, case) + println("outputDir==$outputDir") + generator.generate(androidManifest, outputDir, "test") + + val cmd = "javac -cp ../runtime/build/classes/java/main:build/classes/java/test" + + " ${outputDir.absolutePath}/test/PluginManifest.java" + val process = Runtime.getRuntime().exec(cmd) + val ret = process.waitFor() + Assert.assertEquals(cmd, 0, ret) + } +} \ No newline at end of file diff --git a/projects/sdk/core/manifest-parser/src/test/resources/case_as_little_as_possible.xml b/projects/sdk/core/manifest-parser/src/test/resources/case_as_little_as_possible.xml new file mode 100644 index 000000000..b5308d2f4 --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/resources/case_as_little_as_possible.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/projects/sdk/core/manifest-parser/src/test/resources/noAppComponentFactory.xml b/projects/sdk/core/manifest-parser/src/test/resources/noAppComponentFactory.xml new file mode 100644 index 000000000..f5b0162c2 --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/resources/noAppComponentFactory.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/projects/sdk/core/manifest-parser/src/test/resources/sample-app.xml b/projects/sdk/core/manifest-parser/src/test/resources/sample-app.xml new file mode 100644 index 000000000..ac4789f54 --- /dev/null +++ b/projects/sdk/core/manifest-parser/src/test/resources/sample-app.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginManifest.java b/projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginManifest.java new file mode 100644 index 000000000..a4bfb5972 --- /dev/null +++ b/projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginManifest.java @@ -0,0 +1,142 @@ +package com.tencent.shadow.core.runtime; + +import android.os.Parcel; +import android.os.Parcelable; + +public interface PluginManifest { + /** + * same as android.content.pm.PackageItemInfo#packageName + */ + String getApplicationPackageName(); + + /** + * same as android.content.pm.ApplicationInfo#className + */ + String getApplicationClassName(); + + /** + * same as android.content.pm.ApplicationInfo#appComponentFactory + */ + String getAppComponentFactory(); + + /** + * same as android.content.pm.ApplicationInfo#theme + */ + int getApplicationTheme(); + + ActivityInfo[] getActivities(); + + ServiceInfo[] getServices(); + + ReceiverInfo[] getReceivers(); + + ProviderInfo[] getProviders(); + + abstract class ComponentInfo implements Parcelable { + public final String className; + + public ComponentInfo(String className) { + this.className = className; + } + + protected ComponentInfo(Parcel in) { + className = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(className); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ComponentInfo createFromParcel(Parcel in) { + throw new UnsupportedOperationException(); + } + + @Override + public ComponentInfo[] newArray(int size) { + return new ComponentInfo[size]; + } + }; + } + + final class ActivityInfo extends ComponentInfo implements Parcelable { + public final int theme; + public final int configChanges; + public final int softInputMode; + + public ActivityInfo(String className, + int theme, + int configChanges, + int softInputMode) { + super(className); + this.theme = theme; + this.configChanges = configChanges; + this.softInputMode = softInputMode; + } + + protected ActivityInfo(Parcel in) { + super(in); + theme = in.readInt(); + configChanges = in.readInt(); + softInputMode = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(theme); + dest.writeInt(configChanges); + dest.writeInt(softInputMode); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ActivityInfo createFromParcel(Parcel in) { + return new ActivityInfo(in); + } + + @Override + public ActivityInfo[] newArray(int size) { + return new ActivityInfo[size]; + } + }; + } + + final class ServiceInfo extends ComponentInfo { + + public ServiceInfo(String className) { + super(className); + } + } + + final class ReceiverInfo extends ComponentInfo { + public final String[] actions; + + public ReceiverInfo(String className, String[] actions) { + super(className); + this.actions = actions; + } + } + + final class ProviderInfo extends ComponentInfo { + public final String authorities; + + public ProviderInfo(String className, String authorities) { + super(className); + this.authorities = authorities; + } + } + +} diff --git a/projects/sdk/core/settings.gradle b/projects/sdk/core/settings.gradle index cfcdf1214..b36ae5451 100644 --- a/projects/sdk/core/settings.gradle +++ b/projects/sdk/core/settings.gradle @@ -7,6 +7,7 @@ include 'loader', 'common', 'manager', 'manager-db-test', + 'manifest-parser', 'load-parameters', 'transform-kit', 'utils' \ No newline at end of file diff --git a/projects/test/plugin/androidx-cases/test-plugin-androidx-cases/build.gradle b/projects/test/plugin/androidx-cases/test-plugin-androidx-cases/build.gradle index d2ff1bd4c..e09ae67a7 100644 --- a/projects/test/plugin/androidx-cases/test-plugin-androidx-cases/build.gradle +++ b/projects/test/plugin/androidx-cases/test-plugin-androidx-cases/build.gradle @@ -41,6 +41,10 @@ dependencies { implementation "androidx.activity:activity:$activity_version" implementation "androidx.appcompat:appcompat:$appcompat_version" + + //Shadow Transform后业务代码会有一部分实际引用runtime中的类 + //如果不以compileOnly方式依赖,会导致其他Transform或者Proguard找不到这些类 + pluginCompileOnly 'com.tencent.shadow.core:runtime' } buildscript { diff --git a/projects/test/plugin/general-cases/test-plugin-general-cases/build.gradle b/projects/test/plugin/general-cases/test-plugin-general-cases/build.gradle index b90858c0c..9c0de39bf 100644 --- a/projects/test/plugin/general-cases/test-plugin-general-cases/build.gradle +++ b/projects/test/plugin/general-cases/test-plugin-general-cases/build.gradle @@ -49,6 +49,10 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-remote:3.3.0' androidTestImplementation "androidx.test:runner:1.3.0" + + //Shadow Transform后业务代码会有一部分实际引用runtime中的类 + //如果不以compileOnly方式依赖,会导致其他Transform或者Proguard找不到这些类 + pluginCompileOnly 'com.tencent.shadow.core:runtime' } buildscript { diff --git a/projects/test/plugin/particular-cases/plugin-service-for-host/build.gradle b/projects/test/plugin/particular-cases/plugin-service-for-host/build.gradle index e28968788..96ffb3eca 100644 --- a/projects/test/plugin/particular-cases/plugin-service-for-host/build.gradle +++ b/projects/test/plugin/particular-cases/plugin-service-for-host/build.gradle @@ -34,6 +34,12 @@ android { } } +dependencies { + //Shadow Transform后业务代码会有一部分实际引用runtime中的类 + //如果不以compileOnly方式依赖,会导致其他Transform或者Proguard找不到这些类 + pluginCompileOnly 'com.tencent.shadow.core:runtime' +} + buildscript { repositories { if (!System.getenv().containsKey("DISABLE_TENCENT_MAVEN_MIRROR")) {