blob: 6bba4a7d2830a593f832bf1256413998fb6bc4f1 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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.
*/
import androidx.build.LibraryType
import androidx.build.Release
import androidx.build.metalava.MetalavaRunnerKt
import androidx.build.uptodatedness.EnableCachingKt
import androidx.build.Version
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.api.SourceKind
import com.google.common.io.Files
import org.apache.commons.io.FileUtils
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import java.util.concurrent.TimeUnit
import javax.inject.Inject
buildscript {
dependencies {
// This dependency means that tasks in this project might become out-of-date whenever
// certain classes in buildSrc change, and should generally be avoided.
// See b/140265324 for more information
classpath(project.files("${project.rootProject.ext["outDir"]}/buildSrc/private/build/libs/private.jar"))
}
}
plugins {
id("AndroidXPlugin")
id("com.android.library")
id("androidx.stableaidl")
}
dependencies {
// OnBackPressedDispatcher is part of the API
api("androidx.activity:activity:1.2.0")
implementation(libs.guavaAndroid)
implementation("androidx.annotation:annotation:1.2.0")
implementation("androidx.core:core:1.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel:2.2.0")
implementation ("androidx.media:media:1.6.0")
// Session and Screen both implement LifeCycleOwner so this needs to be exposed.
api("androidx.lifecycle:lifecycle-common-java8:2.2.0")
api("androidx.annotation:annotation-experimental:1.3.1")
annotationProcessor(libs.nullaway)
// TODO(shiufai): We need this for assertThrows. Point back to the AndroidX shared version if
// it is ever upgraded.
testImplementation(libs.junit)
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
testImplementation project(path: ':car:app:app-testing')
}
project.ext {
latestCarAppApiLevel = "7"
}
android {
buildFeatures {
resValues = true
aidl = true
}
defaultConfig {
minSdkVersion 21
multiDexEnabled = true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "car_app_library_version", androidx.LibraryVersions.CAR_APP.toString()
consumerProguardFiles "proguard-rules.pro"
}
buildTypes.all {
stableAidl {
version 1
}
}
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.car.app"
}
androidx {
name = "Android for Cars App"
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2020"
description = "Build navigation, parking, and charging apps for Android Auto"
metalavaK2UastEnabled = true
}
// Use MetalavaRunnerKt to execute Metalava operations. MetalavaRunnerKt is defined in the buildSrc
// project and provides convience methods for interacting with Metalava. Configruation required
// for MetalavaRunnerKt is taken from buildSrc/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt.
abstract class ProtocolApiTask extends DefaultTask {
private final WorkerExecutor workerExecutor
@Classpath
public FileCollection metalavaClasspath
@Classpath
public FileCollection bootClasspath
@Classpath
public FileCollection dependencyClasspath
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public FileCollection sourceDirs
@Input
public Property<KotlinVersion> kotlinSourceLevel
@Inject
ProtocolApiTask(WorkerExecutor workerExecutor, ObjectFactory objects) {
this.workerExecutor = workerExecutor
this.kotlinSourceLevel = objects.property(KotlinVersion)
}
@Internal
def runMetalava(List<String> additionalArgs) {
List<String> standardArgs = [
"--classpath",
(bootClasspath.files + dependencyClasspath.files).join(File.pathSeparator),
'--source-path',
sourceDirs.filter { it.exists() }.join(File.pathSeparator),
'--format=v4',
'--quiet'
]
standardArgs.addAll(additionalArgs)
MetalavaRunnerKt.runMetalavaWithArgs(
metalavaClasspath,
standardArgs,
/* k2UastEnabled= */ false,
kotlinSourceLevel.get(),
workerExecutor,
)
}
}
// Use Metalava to generate an API signature file that only includes public API that is annotated
// with @androidx.car.app.annotations.CarProtocol
abstract class GenerateProtocolApiTask extends ProtocolApiTask {
@Inject
GenerateProtocolApiTask(WorkerExecutor workerExecutor, ObjectFactory objects) {
super(workerExecutor, objects)
}
@OutputFile
File generatedApi
@TaskAction
def exec() {
List<String> args = [
'--api',
generatedApi.toString(),
"--show-annotation",
"androidx.car.app.annotations.CarProtocol",
"--hide",
"UnhiddenSystemApi"
]
runMetalava(args)
}
}
// Compare two files and throw an exception if they are not equivalent. This task is used to check
// for diffs to generated Metalava API signature files, which would indicate a protocol API change.
abstract class CheckProtocolApiTask extends DefaultTask {
@InputFile
@Optional
File currentApi
@InputFile
@Optional
File previousApi
@InputFile
File generatedApi
def summarizeDiff(File a, File b) {
if (!a.exists()) {
return "$a does not exist"
}
if (!b.exists()) {
return "$b does not exist"
}
Process process = new ProcessBuilder(Arrays.asList("diff", a.toString(), b.toString()))
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.start()
process.waitFor(5, TimeUnit.SECONDS)
List<String> diffLines = process.inputStream.newReader().readLines()
int maxSummaryLines = 50
if (diffLines.size() > maxSummaryLines) {
diffLines = diffLines.subList(0, maxSummaryLines)
diffLines.add("[long diff was truncated]")
}
return String.join("\n", diffLines)
}
@TaskAction
def exec() {
if (currentApi == null && previousApi == null) {
return
}
File apiFile
if (currentApi != null) {
apiFile = currentApi
} else {
apiFile = previousApi
}
if (!FileUtils.contentEquals(apiFile, generatedApi)) {
String diff = summarizeDiff(apiFile, generatedApi)
String message = """API definition has changed
Declared definition is $apiFile
True definition is $generatedApi
Please run `./gradlew updateProtocolApi` to confirm these changes are
intentional by updating the API definition.
Difference between these files:
$diff"""
throw new GradleException("Protocol changes detected!\n$message")
}
}
}
// Check for compatibility breaking changes between two Metalava API signature files. This task is
// used to detect backward-compatibility breaking changes to protocol API.
abstract class CheckProtocolApiCompatibilityTask extends ProtocolApiTask {
@Inject
CheckProtocolApiCompatibilityTask(WorkerExecutor workerExecutor, ObjectFactory objects) {
super(workerExecutor, objects)
}
@InputFile
@Optional
File previousApi
@InputFile
@Optional
File generatedApi
@TaskAction
def exec() {
if (previousApi == null || generatedApi == null) {
return
}
List<String> args = [
'--source-files',
generatedApi.toString(),
"--check-compatibility:api:released",
previousApi.toString()
]
runMetalava(args)
}
}
// Update protocol API signature file for current Car API level to reflect the state of current
// protocol API in the project.
class UpdateProtocolApiTask extends DefaultTask {
@Internal
File protocolDir
@InputFile
File generatedApi
@Internal
File currentApi
@TaskAction
def exec() {
// The expected Car protocol API signature file for the current Car API level and project
// version
File updatedApi = new File(protocolDir, String.format(
"protocol-%s_%s.txt", project.latestCarAppApiLevel, project.version))
if (currentApi != null && FileUtils.contentEquals(currentApi, generatedApi)) {
return
}
// Determine whether this API level was previously released by checking whether the project
// version matches
boolean alreadyReleased = currentApi != updatedApi
// Determine whether this API level is final (Only snapshot, dev, alpha releases are
// non-final)
boolean isCurrentApiFinal = false
if (currentApi != null) {
isCurrentApiFinal = ProtocolLocation.parseVersion(currentApi).isFinalApi()
}
if (currentApi != null && alreadyReleased && isCurrentApiFinal) {
throw new GradleException("Version has changed for current Car API level. Increment " +
"Car API level before making protocol API changes")
}
// Create new protocol API signature file for current Car API level
Files.copy(generatedApi, updatedApi)
}
}
class ApiLevelFileWriterTask extends DefaultTask {
@Input
String carApiLevel = project.latestCarAppApiLevel
@OutputDirectory
final DirectoryProperty outputDir = project.objects.directoryProperty()
@TaskAction
def exec() {
def outputFile = new File(outputDir.get().asFile, "car-app-api.level")
outputFile.parentFile.mkdirs()
PrintWriter writer = new PrintWriter(outputFile)
writer.println(carApiLevel)
writer.close()
}
}
// Paths and file locations required for protocol API operations
class ProtocolLocation {
File buildDir
File protocolDir
File generatedApi
File currentApi
File previousApi
static Version parseVersion(File file) {
int versionStart = file.name.indexOf("_")
int versionEnd = file.name.indexOf(".txt")
String parsedCurrentVersion = file.name.substring(versionStart + 1, versionEnd)
return new Version(parsedCurrentVersion)
}
def getProtocolApiFile(int carApiLevel) {
File[] apiFiles = protocolDir.listFiles(new FilenameFilter() {
boolean accept(File dir, String name) {
return name.startsWith(String.format("protocol-%d_", carApiLevel))
}
})
if (apiFiles == null || apiFiles.size() == 0) {
return null
} else if (apiFiles.size() > 1) {
File latestApiFile = apiFiles[0]
Version latestVersion = parseVersion(latestApiFile)
for (int i = 1; i < apiFiles.size(); i++) {
File file = apiFiles[i]
Version fileVersion = parseVersion(file)
if (fileVersion > latestVersion) {
latestApiFile = file
latestVersion = fileVersion
}
}
return latestApiFile
}
return apiFiles[0]
}
ProtocolLocation(Project project) {
buildDir = new File(project.buildDir, "/protocol/")
generatedApi = new File(buildDir, "/generated.txt")
protocolDir = new File(project.projectDir, "/protocol/")
int currentApiLevel = Integer.parseInt(project.latestCarAppApiLevel)
currentApi = getProtocolApiFile(currentApiLevel)
previousApi = getProtocolApiFile(currentApiLevel - 1)
}
}
def getLibraryExtension() {
return project.extensions.findByType(LibraryExtension.class)
}
def getLibraryVariant() {
LibraryExtension extension = getLibraryExtension()
return extension.libraryVariants.find({
it.name == Release.DEFAULT_PUBLISH_CONFIG
})
}
public FileCollection getSourceCollection() {
def taskDependencies = new ArrayList<Object>()
def sourceFiles = project.provider {
getLibraryVariant().getSourceFolders(SourceKind.JAVA).collect { folder ->
for (builtBy in folder.builtBy) {
taskDependencies.add(builtBy)
}
folder.dir
}
}
def sourceCollection = project.files(sourceFiles)
for (dep in taskDependencies) {
sourceCollection.builtBy(dep)
}
return sourceCollection
}
def writeCarApiLevel = tasks.register("writeCarApiLevelFile", ApiLevelFileWriterTask)
androidComponents {
onVariants(selector().all(), { variant ->
variant.sources.resources.addGeneratedSourceDirectory(writeCarApiLevel, { it.outputDir })
})
}
// afterEvaluate required to read extension properties
afterEvaluate {
FileCollection sourceCollection = getSourceCollection()
FileCollection dependencyClasspath = getLibraryVariant().getCompileClasspath(null)
FileCollection metalavaClasspath = MetalavaRunnerKt.getMetalavaClasspath(project)
FileCollection bootClasspath = project.files(getLibraryExtension().bootClasspath)
Provider<KotlinVersion> kotlinSourceLevel = androidx.kotlinApiVersion
ProtocolLocation projectProtocolLocation = new ProtocolLocation(project)
tasks.register("generateProtocolApi", GenerateProtocolApiTask) { task ->
task.description = "Generate an API signature file for the classes annotated with @CarProtocol"
task.generatedApi = projectProtocolLocation.generatedApi
task.dependsOn(assemble)
task.sourceDirs = sourceCollection
task.dependencyClasspath = dependencyClasspath
task.metalavaClasspath = metalavaClasspath
task.bootClasspath = bootClasspath
task.kotlinSourceLevel.set(kotlinSourceLevel)
}
tasks.register("checkProtocolApiCompat", CheckProtocolApiCompatibilityTask) { task ->
task.description = "Check for BREAKING changes to the protocol API"
task.previousApi = projectProtocolLocation.previousApi
task.generatedApi = projectProtocolLocation.generatedApi
task.dependsOn(generateProtocolApi)
task.sourceDirs = sourceCollection
task.dependencyClasspath = dependencyClasspath
task.metalavaClasspath = metalavaClasspath
task.bootClasspath = bootClasspath
task.kotlinSourceLevel.set(kotlinSourceLevel)
}
tasks.register("checkProtocolApi", CheckProtocolApiTask) { task ->
task.description = "Check for changes to the protocol API"
task.generatedApi = projectProtocolLocation.generatedApi
task.previousApi = projectProtocolLocation.previousApi
task.currentApi = projectProtocolLocation.currentApi
task.dependsOn(checkProtocolApiCompat)
}
tasks.register("updateProtocolApi", UpdateProtocolApiTask) { task ->
task.description = "Update protocol API signature file for current Car API level to reflect" +
"the current state of the protocol API in the source tree."
task.protocolDir = projectProtocolLocation.protocolDir
task.generatedApi = projectProtocolLocation.generatedApi
task.currentApi = projectProtocolLocation.currentApi
task.dependsOn(checkProtocolApiCompat)
}
EnableCachingKt.cacheEvenIfNoOutputs(checkProtocolApi)
EnableCachingKt.cacheEvenIfNoOutputs(checkProtocolApiCompat)
checkApi.dependsOn(checkProtocolApi)
}