package com.didi.virtualapk.hooker import com.android.build.gradle.AndroidConfig import com.android.build.gradle.AppExtension import com.android.build.gradle.api.ApkVariant import com.android.build.gradle.internal.scope.TaskOutputHolder import com.android.build.gradle.tasks.ProcessAndroidResources import com.android.sdklib.BuildToolInfo import com.didi.virtualapk.Constants import com.didi.virtualapk.aapt.Aapt import com.didi.virtualapk.collector.ResourceCollector import com.didi.virtualapk.collector.res.ResourceEntry import com.didi.virtualapk.collector.res.StyleableEntry import com.didi.virtualapk.utils.FileUtil import com.didi.virtualapk.utils.Log import com.didi.virtualapk.utils.Reflect import com.google.common.collect.ListMultimap import com.google.common.io.Files import org.gradle.api.Project /** * Filter the host resources out of the plugin apk. * Modify the .arsc file to delete host element, * rearrange plugin element, hold the new resource IDs * * @author zhengtao */ class ProcessResourcesHooker extends GradleTaskHooker { /** * Collector to gather the sources and styleables */ ResourceCollector resourceCollector /** * Android config information specified in build.gradle */ AndroidConfig androidConfig ProcessResourcesHooker(Project project, ApkVariant apkVariant) { super(project, apkVariant) androidConfig = project.extensions.findByType(AppExtension) } @Override String getTaskName() { return scope.getTaskName('process', 'Resources') } @Override void beforeTaskExecute(ProcessAndroidResources aaptTask) { } /** * Since we need to remove the host resources and modify the resource ID, * we will reedit the AP_ file and repackage it after the task execute * * @param par Gradle task of process android resources */ @Override void afterTaskExecute(ProcessAndroidResources par) { if (project.extensions.extraProperties.get(Constants.GRADLE_3_1_0)) { File outputFile = Reflect.on('com.android.build.gradle.internal.scope.ExistingBuildElements') .call('from', TaskOutputHolder.TaskOutputType.PROCESSED_RES, scope.getOutput(TaskOutputHolder.TaskOutputType.PROCESSED_RES)) .call('element', variantData.outputScope.mainSplit) .call('getOutputFile') .get() repackage(par, outputFile) } else { variantData.outputScope.getOutputs(TaskOutputHolder.TaskOutputType.PROCESSED_RES).each { repackage(par, it.outputFile) } } } void repackage(ProcessAndroidResources par, File apFile) { def resourcesDir = new File(apFile.parentFile, Files.getNameWithoutExtension(apFile.name)) /* * Clean up resources merge directory */ resourcesDir.deleteDir() File backupFile = new File(apFile.getParentFile(), "${Files.getNameWithoutExtension(apFile.name)}-original.${Files.getFileExtension(apFile.name)}") backupFile.delete() project.copy { from apFile into apFile.getParentFile() rename { backupFile.name } } /* * Unzip resources-${variant.name}.ap_ */ project.copy { from project.zipTree(apFile) into resourcesDir include 'AndroidManifest.xml' include 'resources.arsc' include 'res/**/*' } // File backupDir = new File(resourcesDir.parentFile, resourcesDir.name + '-original') // backupDir.deleteDir() // project.copy { // from project.fileTree(resourcesDir) // into backupDir // } resourceCollector = new ResourceCollector(project, par) resourceCollector.collect() def retainedTypes = convertResourcesForAapt(resourceCollector.pluginResources) def retainedStylealbes = convertStyleablesForAapt(resourceCollector.pluginStyleables) def resIdMap = resourceCollector.resIdMap def rSymbolFile = par.textSymbolOutputFile def libRefTable = ["${virtualApk.packageId}": par.applicationId] def filteredResources = [] as HashSet def updatedResources = [] as HashSet def aapt = new Aapt(resourcesDir, rSymbolFile, androidConfig.buildToolsRevision) //Delete host resources, must do it before filterPackage aapt.filterResources(retainedTypes, filteredResources) //Modify the arsc file, and replace ids of related xml files aapt.filterPackage(retainedTypes, retainedStylealbes, virtualApk.packageId, resIdMap, libRefTable, updatedResources) File hostDir = vaContext.getBuildDir(scope) FileUtil.saveFile(hostDir, "${taskName}-retainedTypes", retainedTypes) FileUtil.saveFile(hostDir, "${taskName}-retainedStylealbes", retainedStylealbes) FileUtil.saveFile(hostDir, "${taskName}-filteredResources", true, filteredResources) FileUtil.saveFile(hostDir, "${taskName}-updatedResources", true, updatedResources) /* * Delete filtered entries and then add updated resources into resources-${variant.name}.ap_ */ com.didi.virtualapk.utils.ZipUtil.with(apFile).deleteAll(filteredResources + updatedResources) project.exec { executable par.buildTools.getPath(BuildToolInfo.PathId.AAPT) workingDir resourcesDir args 'add', apFile.path args updatedResources standardOutput = System.out errorOutput = System.err } updateRJava(aapt, par.sourceOutputDir) mark() } /** * Because the resource ID has changed, we need to regenerate the R.java file, * include the all resources R, plugin resources R, and R files of retained aars * * @param aapt Class to expand aapt function * @param sourceOutputDir Directory of R.java files generated by aapt * */ def updateRJava(Aapt aapt, File sourceOutputDir) { File vaBuildDir = vaContext.getBuildDir(scope) File backupDir = new File(vaBuildDir, "origin/r") project.ant.move(todir: backupDir) { fileset(dir: sourceOutputDir) { include(name: '**/R.java') } } FileUtil.deleteEmptySubfolders(sourceOutputDir) def rSourceFile = new File(sourceOutputDir, "${vaContext.packagePath}${File.separator}R.java") aapt.generateRJava(rSourceFile, apkVariant.applicationId, resourceCollector.allResources, resourceCollector.allStyleables) Log.i 'ProcessResourcesHooker', "Updated R.java: ${rSourceFile.absoluteFile}" def splitRSourceFile = new File(vaBuildDir, "source${File.separator}r${File.separator}${vaContext.packagePath}${File.separator}R.java") aapt.generateRJava(splitRSourceFile, apkVariant.applicationId, resourceCollector.pluginResources, resourceCollector.pluginStyleables) Log.i 'ProcessResourcesHooker', "Updated R.java: ${splitRSourceFile.absoluteFile}" vaContext.splitRJavaFile = splitRSourceFile vaContext.retainedAarLibs.each { def aarPackage = it.package def rJavaFile = new File(sourceOutputDir, "${aarPackage.replace('.'.charAt(0), File.separatorChar)}${File.separator}R.java") aapt.generateRJava(rJavaFile, aarPackage, it.aarResources, it.aarStyleables) Log.i 'ProcessResourcesHooker', "Updated R.java: ${rJavaFile.absoluteFile}" } } /** * We use the third party library to modify the ASRC file, * this method used to transform resource data into the structure of the library * @param pluginResources Map of plugin resources */ def convertResourcesForAapt(ListMultimap pluginResources) { def retainedTypes = [] pluginResources.keySet().each { resType -> def firstEntry = pluginResources.get(resType).get(0) def typeEntry = [ type: "int", name: resType, id: parseTypeIdFromResId(firstEntry.resourceId), _id: parseTypeIdFromResId(firstEntry.newResourceId), entries: []] pluginResources.get(resType).each { resEntry -> typeEntry.entries.add([ name : resEntry.resourceName, id : parseEntryIdFromResId(resEntry.resourceId), _id: parseEntryIdFromResId(resEntry.newResourceId), v : resEntry.resourceId, _v : resEntry.newResourceId, vs: resEntry.hexResourceId, _vs : resEntry.hexNewResourceId]) } retainedTypes.add(typeEntry) } retainedTypes.sort { t1, t2 -> t1._id - t2._id } return retainedTypes } /** * Transform styleable data into the structure of the aapt library * @param pluginStyleables Map of plugin styleables */ def convertStyleablesForAapt(List pluginStyleables) { def retainedStyleables = [] pluginStyleables.each { styleableEntry -> retainedStyleables.add([vtype : styleableEntry.valueType, type : 'styleable', key : styleableEntry.name, idStr : styleableEntry.value]) } return retainedStyleables } /** * Parse the type part of a android resource id */ def parseTypeIdFromResId(int resourceId) { resourceId >> 16 & 0xFF } /** * Parse the entry part of a android resource id */ def parseEntryIdFromResId(int resourceId) { resourceId & 0xFFFF } }