blob: 2354879302664ca222f6dcc7dcd7227d39da072b [file] [log] [blame]
/*
* Copyright (C) 2018 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.
*/
package com.android.build.gradle.internal.transforms
import com.android.build.api.component.impl.ComponentPropertiesImpl
import com.android.build.gradle.internal.pipeline.StreamFilter
import com.android.build.gradle.internal.scope.ApkData
import com.android.build.gradle.internal.scope.ExistingBuildElements
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.build.gradle.internal.scope.MultipleArtifactType
import com.android.build.gradle.internal.scope.VariantScope
import com.android.build.gradle.internal.tasks.NonIncrementalTask
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction
import com.android.build.gradle.options.BooleanOption
import com.android.build.gradle.tasks.ResourceUsageAnalyzer
import com.android.utils.FileUtils
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.LogLevel
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskProvider
import org.xml.sax.SAXException
import java.io.File
import java.io.IOException
import javax.xml.parsers.ParserConfigurationException
/**
* Task to shrink resources for the android app bundle
*/
abstract class ShrinkBundleResourcesTask : NonIncrementalTask() {
@get:OutputFile
abstract val compressedResources: RegularFileProperty
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val uncompressedResources: RegularFileProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
lateinit var dex: FileCollection
private set
@get:Optional
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val lightRClasses: RegularFileProperty
@get:Optional
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
abstract val rTxtFile: RegularFileProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val resourceDir: DirectoryProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:Optional
abstract val mappingFileSrc: RegularFileProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val mergedManifests: DirectoryProperty
@get:Input
abstract val enableRTxtResourceShrinking: Property<Boolean>
private lateinit var mainSplit: ApkData
override fun doTaskAction() {
val uncompressedResourceFile = uncompressedResources.get().asFile
val compressedResourceFile= compressedResources.get().asFile
val classes = dex.files
var reportFile: File? = null
val mappingFile = mappingFileSrc.orNull?.asFile
if (mappingFile != null) {
val logDir = mappingFile.parentFile
if (logDir != null) {
reportFile = File(logDir, "resources.txt")
}
}
FileUtils.mkdirs(compressedResourceFile.parentFile)
val rSource = if (enableRTxtResourceShrinking.get()){
rTxtFile.get().asFile
} else {
lightRClasses.get().asFile
}
val manifestFile = ExistingBuildElements.from(InternalArtifactType.BUNDLE_MANIFEST, mergedManifests)
.element(mainSplit)
?.outputFile
?: throw RuntimeException("Cannot find merged manifest file")
// Analyze resources and usages and strip out unused
val analyzer = ResourceUsageAnalyzer(
rSource,
classes,
manifestFile,
mappingFile,
resourceDir.get().asFile,
reportFile,
ResourceUsageAnalyzer.ApkFormat.PROTO
)
try {
analyzer.isVerbose = logger.isEnabled(LogLevel.INFO)
analyzer.isDebug = logger.isEnabled(LogLevel.DEBUG)
try {
analyzer.analyze()
} catch (e: IOException) {
throw RuntimeException(e)
} catch (e: ParserConfigurationException) {
throw RuntimeException(e)
} catch (e: SAXException) {
throw RuntimeException(e)
}
// Just rewrite the .ap_ file to strip out the res/ files for unused resources
analyzer.rewriteResourceZip(uncompressedResourceFile, compressedResourceFile)
// Dump some stats
val unused = analyzer.unusedResourceCount
if (unused > 0) {
val sb = StringBuilder(200)
sb.append("Removed unused resources")
// This is a bit misleading until we can strip out all resource types:
//int total = analyzer.getTotalResourceCount()
//sb.append("(" + unused + "/" + total + ")")
val before = uncompressedResourceFile.length()
val after = compressedResourceFile.length()
val percent = ((before - after) * 100 / before).toInt().toLong()
sb.append(": Binary resource data reduced from ").append(toKbString(before))
.append("KB to ").append(toKbString(after)).append("KB: Removed ")
.append(percent).append("%")
println(sb.toString())
}
} finally {
analyzer.dispose()
}
}
companion object {
private fun toKbString(size: Long): String {
return (size.toInt() / 1024).toString()
}
}
class CreationAction(componentProperties: ComponentPropertiesImpl) :
VariantTaskCreationAction<ShrinkBundleResourcesTask>(
componentProperties
) {
override val name: String = component.computeTaskName("shrink", "Resources")
override val type: Class<ShrinkBundleResourcesTask>
get() = ShrinkBundleResourcesTask::class.java
override fun handleProvider(taskProvider: TaskProvider<out ShrinkBundleResourcesTask>) {
super.handleProvider(taskProvider)
variantScope.artifacts.producesFile(
InternalArtifactType.SHRUNK_LINKED_RES_FOR_BUNDLE,
taskProvider,
ShrinkBundleResourcesTask::compressedResources,
"shrunk-bundled-res.ap_"
)
}
override fun configure(task: ShrinkBundleResourcesTask) {
super.configure(task)
variantScope.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.LINKED_RES_FOR_BUNDLE,
task.uncompressedResources
)
task.mainSplit = component.outputs.getMainSplit().apkData
task.dex = if (variantScope.artifacts.hasFinalProduct(InternalArtifactType.BASE_DEX)) {
variantScope
.artifacts
.getFinalProductAsFileCollection(InternalArtifactType.BASE_DEX).get()
} else if (variantScope.artifacts.hasFinalProducts(MultipleArtifactType.DEX)) {
variantScope.globalScope.project.files(
variantScope.artifacts.getOperations().getAll(MultipleArtifactType.DEX))
} else {
variantScope.transformManager.getPipelineOutputAsFileCollection(StreamFilter.DEX)
}
if (variantScope
.globalScope.projectOptions[BooleanOption.ENABLE_R_TXT_RESOURCE_SHRINKING]) {
variantScope.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.RUNTIME_SYMBOL_LIST,
task.rTxtFile
)
} else {
variantScope.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.COMPILE_AND_RUNTIME_NOT_NAMESPACED_R_CLASS_JAR,
task.lightRClasses
)
}
task.enableRTxtResourceShrinking.set(variantScope.globalScope
.projectOptions[BooleanOption.ENABLE_R_TXT_RESOURCE_SHRINKING])
variantScope.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.MERGED_NOT_COMPILED_RES,
task.resourceDir)
if (variantScope.artifacts.hasFinalProduct(InternalArtifactType.APK_MAPPING))
variantScope.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.APK_MAPPING,
task.mappingFileSrc)
variantScope.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.BUNDLE_MANIFEST,
task.mergedManifests)
}
}
}