| /* |
| * 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. |
| */ |
| |
| /** |
| * This file was created using the `create_project.py` script located in the |
| * `<AndroidX root>/development/project-creator` directory. |
| * |
| * Please use that script when creating a new project, rather than copying an existing project and |
| * modifying its settings. |
| */ |
| 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") |
| id("org.jetbrains.kotlin.android") |
| } |
| |
| 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.4.1") |
| |
| api(libs.kotlinStdlib) |
| implementation(libs.kotlinStdlibCommon) |
| |
| 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.mockitoKotlin4) |
| testImplementation(libs.robolectric) |
| testImplementation(libs.truth) |
| testImplementation project(path: ':car:app:app-testing') |
| } |
| |
| project.ext { |
| latestCarAppApiLevel = "8" |
| } |
| |
| android { |
| buildFeatures { |
| resValues = true |
| aidl = true |
| } |
| defaultConfig { |
| resValue "string", "car_app_library_version", androidx.LibraryVersions.CAR_APP.toString() |
| consumerProguardFiles "proguard-rules.pro" |
| } |
| buildTypes.configureEach { |
| 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 |
| legacyDisableKotlinStrictApiMode = 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) |
| afterEvaluate { |
| checkApi.dependsOn(checkProtocolApi) |
| } |
| } |