Android Extensions: Allow to disable specific features of Android Extensions (#KT-23244)

This commit is contained in:
Yan Zhulanow
2018-05-18 22:29:22 +03:00
parent a70707b72b
commit 614003ad70
15 changed files with 226 additions and 19 deletions
@@ -132,6 +132,9 @@ public class CompilerConfiguration {
else if (object instanceof Map) {
return (T) Collections.unmodifiableMap((Map) object);
}
else if (object instanceof Set) {
return (T) Collections.unmodifiableSet((Set) object);
}
else if (object instanceof Collection) {
return (T) Collections.unmodifiableCollection((Collection) object);
}
@@ -331,6 +331,11 @@ fun getSomething() = 10
@Test
fun testAndroidExtensionsManyVariants() {
if (isLegacyAndroidGradleVersion(androidGradlePluginVersion)) {
// Library dependencies are not supported in older versions of Android Gradle plugin (< 3.0)
return
}
val project = Project("AndroidExtensionsManyVariants", gradleVersion)
val options = defaultBuildOptions().copy(incremental = false)
@@ -346,6 +351,30 @@ fun getSomething() = 10
}
}
@Test
fun testAndroidExtensionsSpecificFeatures() {
val project = Project("AndroidExtensionsSpecificFeatures", gradleVersion)
val options = defaultBuildOptions().copy(incremental = false)
project.build("assemble", options = options) {
assertFailed()
assertContains("Unresolved reference: textView")
}
File(project.projectDir, "app/build.gradle").modify { it.replace("[\"parcelize\"]", "[\"views\"]") }
project.build("assemble", options = options) {
assertFailed()
assertContains("Class 'User' is not abstract and does not implement abstract member public abstract fun writeToParcel")
}
File(project.projectDir, "app/build.gradle").modify { it.replace("[\"views\"]", "[\"parcelize\", \"views\"]") }
project.build("assemble", options = options) {
assertSuccessful()
}
}
@Test
fun testMultiplatformAndroidCompile() = with(Project("multiplatformAndroidProject", gradleVersion)) {
setupWorkingDir()
@@ -36,6 +36,7 @@ android {
androidExtensions {
experimental = true
features = ["views", "parcelize"]
}
dependencies {
@@ -4,6 +4,8 @@ import android.app.Activity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.layout_in_library.text_view
import kotlinx.android.parcel.Parcelize
import android.os.Parcelable
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle) {
@@ -13,3 +15,6 @@ class MainActivity : Activity() {
text_view
}
}
@Parcelize
class User(val firstName: String, val lastName: String) : Parcelable
@@ -0,0 +1,29 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 23
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.example.dagger.kotlin"
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
androidExtensions {
experimental = true
features = ["parcelize"]
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.androidextensions">
<application android:label="app_name">
<activity
android:label="app_name"
android:name="com.example.androidextensions.MyActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.androidextensions
import android.os.Bundle
import android.app.Activity
import kotlinx.android.synthetic.main.activity_main.textView
import kotlinx.android.parcel.Parcelize
import android.os.Parcelable
class HomeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
this.textView.setText("Hello, world!")
}
}
@Parcelize
class User(val firstName: String, val lastName: String) : Parcelable
@@ -0,0 +1,9 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">kotlin</string>
</resources>
@@ -0,0 +1,22 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
mavenLocal()
jcenter()
maven { url 'https://maven.google.com' }
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.android.tools.build:gradle:$android_tools_version"
}
}
allprojects {
repositories {
mavenLocal()
jcenter()
maven { url 'https://maven.google.com' }
}
}
@@ -18,6 +18,7 @@ package org.jetbrains.kotlin.gradle.internal
import groovy.lang.Closure
import org.gradle.util.Configurable
import java.util.*
enum class CacheImplementation(val optionName: String) {
HASH_MAP("hashMap"),
@@ -25,6 +26,19 @@ enum class CacheImplementation(val optionName: String) {
NONE("none")
}
enum class AndroidExtensionsFeature(val featureName: String) {
VIEWS("views"),
PARCELIZE("parcelize");
internal companion object {
internal fun parseFeatures(features: Set<String>): SortedSet<AndroidExtensionsFeature> {
fun find(name: String) = AndroidExtensionsFeature.values().firstOrNull { it.featureName == name }
?: error("Can't find Android Extensions feature $name")
return features.mapTo(sortedSetOf()) { find(it) }
}
}
}
open class AndroidExtensionsExtension : Configurable<AndroidExtensionsExtension> {
private lateinit var onEvaluatedHandler: (AndroidExtensionsExtension) -> Unit
@@ -34,6 +48,8 @@ open class AndroidExtensionsExtension : Configurable<AndroidExtensionsExtension>
open var isExperimental: Boolean = false
open var features: Set<String> = AndroidExtensionsFeature.values().mapTo(mutableSetOf()) { it.featureName }
open var defaultCacheImplementation: CacheImplementation = CacheImplementation.HASH_MAP
override fun configure(closure: Closure<*>): AndroidExtensionsExtension {
@@ -104,6 +104,7 @@ class AndroidSubplugin : KotlinGradleSubplugin<KotlinCompile> {
val sourceSets = androidExtension.sourceSets
val pluginOptions = arrayListOf<SubpluginOption>()
pluginOptions += SubpluginOption("features", AndroidExtensionsFeature.VIEWS.featureName)
val mainSourceSet = sourceSets.getByName("main")
val manifestFile = mainSourceSet.manifest.srcFile
@@ -157,6 +158,8 @@ class AndroidSubplugin : KotlinGradleSubplugin<KotlinCompile> {
androidProjectHandler as? AbstractAndroidProjectHandler<Any?> ?: return emptyList()
val pluginOptions = arrayListOf<SubpluginOption>()
pluginOptions += SubpluginOption("features",
AndroidExtensionsFeature.parseFeatures(androidExtensionsExtension.features).joinToString(",") { it.featureName })
pluginOptions += SubpluginOption("experimental", "true")
pluginOptions += SubpluginOption("defaultCacheImplementation", androidExtensionsExtension.defaultCacheImplementation.optionName)
@@ -208,6 +211,8 @@ class AndroidSubplugin : KotlinGradleSubplugin<KotlinCompile> {
addVariant(sourceSetName, (srcDirs - commonResDirectories).sorted())
}
project.logger.warn("Blah: " + allResDirectories)
return wrapPluginOptions(pluginOptions, "configuration")
}
@@ -54,6 +54,12 @@ object AndroidConfigurationKeys {
val PACKAGE = CompilerConfigurationKey.create<String>("application package fq name")
val EXPERIMENTAL = CompilerConfigurationKey.create<String>("enable experimental features")
val DEFAULT_CACHE_IMPL = CompilerConfigurationKey.create<String>("default cache implementation")
val FEATURES = CompilerConfigurationKey.create<Set<AndroidExtensionsFeature>>("enabled features")
}
enum class AndroidExtensionsFeature(val featureName: String) {
VIEWS("views"),
PARCELIZE("parcelize")
}
class AndroidCommandLineProcessor : CommandLineProcessor {
@@ -68,6 +74,9 @@ class AndroidCommandLineProcessor : CommandLineProcessor {
val DEFAULT_CACHE_IMPL_OPTION = CliOption(
"defaultCacheImplementation", "hashMap/sparseArray/none", "Default cache implementation for module", required = false)
val FEATURES_OPTION = CliOption(
"features", AndroidExtensionsFeature.values().joinToString(" | "), "Enabled features", required = false)
/* This option is just for saving Android Extensions status in Kotlin facet. It should not be supported from CLI. */
val ENABLED_OPTION: CliOption = CliOption("enabled", "true/false", "Enable Android Extensions", required = false)
}
@@ -75,7 +84,7 @@ class AndroidCommandLineProcessor : CommandLineProcessor {
override val pluginId: String = ANDROID_COMPILER_PLUGIN_ID
override val pluginOptions: Collection<CliOption>
= listOf(VARIANT_OPTION, PACKAGE_OPTION, EXPERIMENTAL_OPTION, DEFAULT_CACHE_IMPL_OPTION, CONFIGURATION)
= listOf(VARIANT_OPTION, PACKAGE_OPTION, EXPERIMENTAL_OPTION, DEFAULT_CACHE_IMPL_OPTION, CONFIGURATION, FEATURES_OPTION)
override fun processOption(option: CliOption, value: String, configuration: CompilerConfiguration) {
when (option) {
@@ -84,6 +93,12 @@ class AndroidCommandLineProcessor : CommandLineProcessor {
EXPERIMENTAL_OPTION -> configuration.put(AndroidConfigurationKeys.EXPERIMENTAL, value)
DEFAULT_CACHE_IMPL_OPTION -> configuration.put(AndroidConfigurationKeys.DEFAULT_CACHE_IMPL, value)
CONFIGURATION -> configuration.applyOptionsFrom(decodePluginOptions(value), pluginOptions)
FEATURES_OPTION -> {
val features = value.split(',').mapNotNullTo(mutableSetOf()) {
name -> AndroidExtensionsFeature.values().firstOrNull { it.featureName == name }
}
configuration.put(AndroidConfigurationKeys.FEATURES, features)
}
else -> throw CliOptionProcessingException("Unknown option: ${option.name}")
}
}
@@ -97,6 +112,37 @@ class AndroidComponentRegistrar : ComponentRegistrar {
ClassBuilderInterceptorExtension.registerExtension(project, ParcelableClinitClassBuilderInterceptorExtension())
}
private fun parseVariant(s: String): AndroidVariant? {
val parts = s.split(';')
if (parts.size < 2) return null
return AndroidVariant(parts[0], parts.drop(1))
}
fun registerViewExtensions(configuration: CompilerConfiguration, isExperimental: Boolean, project: MockProject) {
val applicationPackage = configuration.get(AndroidConfigurationKeys.PACKAGE) ?: return
val variants = configuration.get(AndroidConfigurationKeys.VARIANT)?.mapNotNull { parseVariant(it) } ?: return
val globalCacheImpl = parseCacheImplementationType(configuration.get(AndroidConfigurationKeys.DEFAULT_CACHE_IMPL))
if (variants.isEmpty() || applicationPackage.isEmpty()) {
return
}
val layoutXmlFileManager = CliAndroidLayoutXmlFileManager(project, applicationPackage, variants)
project.registerService(AndroidLayoutXmlFileManager::class.java, layoutXmlFileManager)
ExpressionCodegenExtension.registerExtension(project,
CliAndroidExtensionsExpressionCodegenExtension(isExperimental, globalCacheImpl))
StorageComponentContainerContributor.registerExtension(project,
AndroidExtensionPropertiesComponentContainerContributor())
ClassBuilderInterceptorExtension.registerExtension(project,
CliAndroidOnDestroyClassBuilderInterceptorExtension(globalCacheImpl))
PackageFragmentProviderExtension.registerExtension(project,
CliAndroidPackageFragmentProviderExtension(isExperimental))
}
fun parseCacheImplementationType(s: String?): CacheImplementation = when (s) {
"sparseArray" -> CacheImplementation.SPARSE_ARRAY
"none" -> CacheImplementation.NO_CACHE
@@ -105,31 +151,17 @@ class AndroidComponentRegistrar : ComponentRegistrar {
}
override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
val applicationPackage = configuration.get(AndroidConfigurationKeys.PACKAGE) ?: return
val variants = configuration.get(AndroidConfigurationKeys.VARIANT)?.mapNotNull { parseVariant(it) } ?: return
val features = configuration.get(AndroidConfigurationKeys.FEATURES) ?: AndroidExtensionsFeature.values().toSet()
val isExperimental = configuration.get(AndroidConfigurationKeys.EXPERIMENTAL) == "true"
val globalCacheImpl = parseCacheImplementationType(configuration.get(AndroidConfigurationKeys.DEFAULT_CACHE_IMPL))
if (isExperimental) {
if (isExperimental && AndroidExtensionsFeature.PARCELIZE in features) {
registerParcelExtensions(project)
}
if (variants.isNotEmpty() && !applicationPackage.isNullOrBlank()) {
val layoutXmlFileManager = CliAndroidLayoutXmlFileManager(project, applicationPackage!!, variants)
project.registerService(AndroidLayoutXmlFileManager::class.java, layoutXmlFileManager)
ExpressionCodegenExtension.registerExtension(project, CliAndroidExtensionsExpressionCodegenExtension(isExperimental, globalCacheImpl))
StorageComponentContainerContributor.registerExtension(project, AndroidExtensionPropertiesComponentContainerContributor())
ClassBuilderInterceptorExtension.registerExtension(project, CliAndroidOnDestroyClassBuilderInterceptorExtension(globalCacheImpl))
PackageFragmentProviderExtension.registerExtension(project, CliAndroidPackageFragmentProviderExtension(isExperimental))
if (AndroidExtensionsFeature.VIEWS in features) {
registerViewExtensions(configuration, isExperimental, project)
}
}
private fun parseVariant(s: String): AndroidVariant? {
val parts = s.split(';')
if (parts.size < 2) return null
return AndroidVariant(parts[0], parts.drop(1))
}
}
class AndroidExtensionPropertiesComponentContainerContributor : StorageComponentContainerContributor {