1973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets/*
2973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * Copyright 2018 The Android Open Source Project
3973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets *
4973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * Licensed under the Apache License, Version 2.0 (the "License");
5973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * you may not use this file except in compliance with the License.
6973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * You may obtain a copy of the License at
7973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets *
8973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets *      http://www.apache.org/licenses/LICENSE-2.0
9973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets *
10973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * Unless required by applicable law or agreed to in writing, software
11973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * distributed under the License is distributed on an "AS IS" BASIS,
12973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * See the License for the specific language governing permissions and
14973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets * limitations under the License.
15973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets */
16973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets
171503d52153986fdcfe7e744795010708b7410892Ian Lakepackage androidx.navigation.safeargs.gradle
18973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets
1928f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinetsimport androidx.navigation.safe.args.generator.ErrorMessage
201503d52153986fdcfe7e744795010708b7410892Ian Lakeimport androidx.navigation.safe.args.generator.generateSafeArgs
217e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinetsimport com.android.build.gradle.internal.tasks.IncrementalTask
2298971a7d72d56aeaf5d997ab251629e2550f350cSergey Vasilinetsimport com.android.ide.common.resources.FileStatus
237e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinetsimport com.google.gson.Gson
247e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinetsimport com.google.gson.reflect.TypeToken
25973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinetsimport org.gradle.api.GradleException
26973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinetsimport org.gradle.api.tasks.Input
27973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinetsimport org.gradle.api.tasks.InputFiles
28973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinetsimport org.gradle.api.tasks.OutputDirectory
29973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinetsimport java.io.File
30973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets
317e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinetsprivate const val MAPPING_FILE = "file_mappings.json"
32973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets
337e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinetsopen class ArgumentsGenerationTask : IncrementalTask() {
34973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets    @get:Input
357e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    lateinit var rFilePackage: String
36fb97caaf48ddb265adca54c03569b2ea7ab5a390Sergey Vasilinets
37fb97caaf48ddb265adca54c03569b2ea7ab5a390Sergey Vasilinets    @get:Input
387e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    lateinit var applicationId: String
39973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets
40973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets    @get:OutputDirectory
417e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    lateinit var outputDir: File
42973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets
43973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets    @get:InputFiles
44973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets    var navigationFiles: List<File> = emptyList()
45973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets
467e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    private fun generateArgs(navFiles: Collection<File>, out: File) = navFiles.map { file ->
4728f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets        val output = generateSafeArgs(rFilePackage, applicationId, file, out)
4828f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets        Mapping(file.relativeTo(project.projectDir).path, output.files) to output.errors
4928f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets    }.unzip().let { (mappings, errorLists) -> mappings to errorLists.flatten() }
507e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets
517e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    private fun writeMappings(mappings: List<Mapping>) {
527e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        File(incrementalFolder, MAPPING_FILE).writer().use { Gson().toJson(mappings, it) }
537e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    }
547e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets
557e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    private fun readMappings(): List<Mapping> {
567e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        val type = object : TypeToken<List<Mapping>>() {}.type
577e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        val mappingsFile = File(incrementalFolder, MAPPING_FILE)
587e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        if (mappingsFile.exists()) {
597e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets            return mappingsFile.reader().use { Gson().fromJson(it, type) }
607e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        } else {
617e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets            return emptyList()
62973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets        }
63973a5c08a9d5a6951830f9dc6c0fa875392491e4Sergey Vasilinets    }
647e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets
657e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    override fun doFullTaskAction() {
667e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        if (outputDir.exists() && !outputDir.deleteRecursively()) {
677e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets            project.logger.warn("Failed to clear directory for navigation arguments")
687e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        }
697e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        if (!outputDir.exists() && !outputDir.mkdirs()) {
707e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets            throw GradleException("Failed to create directory for navigation arguments")
717e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        }
7228f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets        val (mappings, errors) = generateArgs(navigationFiles, outputDir)
737e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        writeMappings(mappings)
7428f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets        failIfErrors(errors)
757e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    }
767e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets
777e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    override fun doIncrementalTaskAction(changedInputs: MutableMap<File, FileStatus>) {
787e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        super.doIncrementalTaskAction(changedInputs)
797e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        val oldMapping = readMappings()
807e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        val navFiles = changedInputs.filter { (_, status) -> status != FileStatus.REMOVED }.keys
8128f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets        val (newMapping, errors) = generateArgs(navFiles, outputDir)
827e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        val newJavaFiles = newMapping.flatMap { it.javaFiles }.toSet()
837e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        val (modified, unmodified) = oldMapping.partition {
847e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets            File(project.projectDir, it.navFile) in changedInputs
857e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        }
867e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        modified.flatMap { it.javaFiles }
877e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets                .filter { name -> name !in newJavaFiles }
887e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets                .forEach { javaName ->
897e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets                    val fileName = "${javaName.replace('.', File.separatorChar)}.java"
907e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets                    val file = File(outputDir, fileName)
917e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets                    if (file.exists()) {
927e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets                        file.delete()
937e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets                    }
947e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets                }
957e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets        writeMappings(unmodified + newMapping)
9628f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets        failIfErrors(errors)
9728f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets    }
9828f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets
9928f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets    private fun failIfErrors(errors: List<ErrorMessage>) {
10028f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets        if (errors.isNotEmpty()) {
10128f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets            val errString = errors.joinToString("\n") { it.toClickableText() }
10228f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets            throw GradleException(
10328f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets                    "androidx.navigation.safeargs plugin failed.\n " +
10428f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets                            "Following errors found: \n$errString")
10528f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets        }
1067e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    }
1077e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets
1087e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets    override fun isIncremental() = true
1097e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets}
1107e7dfaccd4c66e8eec029590c334b3839c3d09b7Sergey Vasilinets
11128f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinetsprivate fun ErrorMessage.toClickableText() = "$path:$line:$column: error: $message"
11228f10aef67d29479eb0821b63967450e1bcee8e4Sergey Vasilinets
11398971a7d72d56aeaf5d997ab251629e2550f350cSergey Vasilinetsprivate data class Mapping(val navFile: String, val javaFiles: List<String>)
114