/* * Tencent is pleased to support the open source community by making Tinker available. * * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved. * * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * * https://opensource.org/licenses/BSD-3-Clause * * 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.tencent.tinker.build.decoder; import com.tencent.tinker.bsdiff.BSDiff; import com.tencent.tinker.build.apkparser.AndroidParser; import com.tencent.tinker.build.info.InfoWriter; import com.tencent.tinker.build.patch.Configuration; import com.tencent.tinker.build.util.FileOperation; import com.tencent.tinker.build.util.Logger; import com.tencent.tinker.build.util.MD5; import com.tencent.tinker.build.util.TinkerPatchException; import com.tencent.tinker.build.util.TypedValue; import com.tencent.tinker.build.util.Utils; import java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; /** * Created by zhangshaowen on 16/8/8. */ public class ResDiffDecoder extends BaseDecoder { private static final String TEST_RESOURCE_NAME = "only_use_to_test_tinker_resource.txt"; private static final String TEST_RESOURCE_ASSETS_PATH = "assets/" + TEST_RESOURCE_NAME; private static final String TEMP_RES_ZIP = "temp_res.zip"; private static final String TEMP_RES_7ZIP = "temp_res_7ZIP.zip"; private final InfoWriter logWriter; private final InfoWriter metaWriter; private ArrayList<String> addedSet; private ArrayList<String> modifiedSet; private ArrayList<String> largeModifiedSet; private HashMap<String, LargeModeInfo> largeModifiedMap; private ArrayList<String> deletedSet; public ResDiffDecoder(Configuration config, String metaPath, String logPath) throws IOException { super(config); if (metaPath != null) { metaWriter = new InfoWriter(config, config.mTempResultDir + File.separator + metaPath); } else { metaWriter = null; } if (logPath != null) { logWriter = new InfoWriter(config, config.mOutFolder + File.separator + logPath); } else { logWriter = null; } addedSet = new ArrayList<>(); modifiedSet = new ArrayList<>(); largeModifiedSet = new ArrayList<>(); largeModifiedMap = new HashMap<>(); deletedSet = new ArrayList<>(); } @Override public void clean() { metaWriter.close(); logWriter.close(); } private boolean checkLargeModFile(File file) { long length = file.length(); if (length > config.mLargeModSize * TypedValue.K_BYTES) { return true; } return false; } @Override public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException { String name = getRelativePathStringToNewFile(newFile); //actually, it won't go below if (newFile == null || !newFile.exists()) { String relativeStringByOldDir = getRelativePathStringToOldFile(oldFile); if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, relativeStringByOldDir)) { Logger.e("found delete resource: " + relativeStringByOldDir + " ,but it match ignore change pattern, just ignore!"); return false; } deletedSet.add(relativeStringByOldDir); writeResLog(newFile, oldFile, TypedValue.DEL); return true; } File outputFile = getOutputPath(newFile).toFile(); if (oldFile == null || !oldFile.exists()) { if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) { Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!"); return false; } FileOperation.copyFileUsingStream(newFile, outputFile); addedSet.add(name); writeResLog(newFile, oldFile, TypedValue.ADD); return true; } //both file length is 0 if (oldFile.length() == 0 && newFile.length() == 0) { return false; } //new add file String newMd5 = MD5.getMD5(newFile); String oldMd5 = MD5.getMD5(oldFile); //oldFile or newFile may be 0b length if (oldMd5 != null && oldMd5.equals(newMd5)) { return false; } if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) { Logger.d("found modify resource: " + name + ", but it match ignore change pattern, just ignore!"); return false; } if (name.equals(TypedValue.RES_MANIFEST)) { Logger.d("found modify resource: " + name + ", but it is AndroidManifest.xml, just ignore!"); return false; } if (name.equals(TypedValue.RES_ARSC)) { if (AndroidParser.resourceTableLogicalChange(config)) { Logger.d("found modify resource: " + name + ", but it is logically the same as original new resources.arsc, just ignore!"); return false; } } dealWithModeFile(name, newMd5, oldFile, newFile, outputFile); return true; } private boolean dealWithModeFile(String name, String newMd5, File oldFile, File newFile, File outputFile) throws IOException { if (checkLargeModFile(newFile)) { if (!outputFile.getParentFile().exists()) { outputFile.getParentFile().mkdirs(); } BSDiff.bsdiff(oldFile, newFile, outputFile); //treat it as normal modify if (Utils.checkBsDiffFileSize(outputFile, newFile)) { LargeModeInfo largeModeInfo = new LargeModeInfo(); largeModeInfo.path = newFile; largeModeInfo.crc = FileOperation.getFileCrc32(newFile); largeModeInfo.md5 = newMd5; largeModifiedSet.add(name); largeModifiedMap.put(name, largeModeInfo); writeResLog(newFile, oldFile, TypedValue.LARGE_MOD); return true; } } modifiedSet.add(name); FileOperation.copyFileUsingStream(newFile, outputFile); writeResLog(newFile, oldFile, TypedValue.MOD); return false; } private void writeResLog(File newFile, File oldFile, int mode) throws IOException { if (logWriter != null) { String log = ""; String relative; switch (mode) { case TypedValue.ADD: relative = getRelativePathStringToNewFile(newFile); Logger.d("Found add resource: " + relative); log = "add resource: " + relative + ", oldSize=" + FileOperation.getFileSizes(oldFile) + ", newSize=" + FileOperation.getFileSizes(newFile); break; case TypedValue.MOD: relative = getRelativePathStringToNewFile(newFile); Logger.d("Found modify resource: " + relative); log = "modify resource: " + relative + ", oldSize=" + FileOperation.getFileSizes(oldFile) + ", newSize=" + FileOperation.getFileSizes(newFile); break; case TypedValue.DEL: relative = getRelativePathStringToOldFile(oldFile); Logger.d("Found deleted resource: " + relative); log = "deleted resource: " + relative + ", oldSize=" + FileOperation.getFileSizes(oldFile) + ", newSize=" + FileOperation.getFileSizes(newFile); break; case TypedValue.LARGE_MOD: relative = getRelativePathStringToNewFile(newFile); Logger.d("Found large modify resource: " + relative + " size:" + newFile.length()); log = "large modify resource: " + relative + ", oldSize=" + FileOperation.getFileSizes(oldFile) + ", newSize=" + FileOperation.getFileSizes(newFile); break; } logWriter.writeLineToInfoFile(log); } } @Override public void onAllPatchesStart() throws IOException, TinkerPatchException { } private void addAssetsFileForTestResource() throws IOException { File dest = new File(config.mTempResultDir + "/" + TEST_RESOURCE_ASSETS_PATH); FileOperation.copyResourceUsingStream(TEST_RESOURCE_NAME, dest); addedSet.add(TEST_RESOURCE_ASSETS_PATH); Logger.d("Add Test resource file: " + TEST_RESOURCE_ASSETS_PATH); String log = "add test resource: " + TEST_RESOURCE_ASSETS_PATH + ", oldSize=" + 0 + ", newSize=" + FileOperation.getFileSizes(dest); logWriter.writeLineToInfoFile(log); } @Override public void onAllPatchesEnd() throws IOException, TinkerPatchException { //only there is only deleted set, we just ignore if (addedSet.isEmpty() && modifiedSet.isEmpty() && largeModifiedSet.isEmpty()) { return; } if (!config.mResRawPattern.contains(TypedValue.RES_ARSC)) { throw new TinkerPatchException("resource must contain resources.arsc pattern"); } if (!config.mResRawPattern.contains(TypedValue.RES_MANIFEST)) { throw new TinkerPatchException("resource must contain AndroidManifest.xml pattern"); } //check gradle build if (config.mUsingGradle) { final boolean ignoreWarning = config.mIgnoreWarning; final boolean resourceArscChanged = modifiedSet.contains(TypedValue.RES_ARSC) || largeModifiedSet.contains(TypedValue.RES_ARSC); if (resourceArscChanged && !config.mUseApplyResource) { if (ignoreWarning) { //ignoreWarning, just log Logger.e("Warning:ignoreWarning is true, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times"); } else { Logger.e("Warning:ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times"); throw new TinkerPatchException( String.format("ignoreWarning is false, but resources.arsc is changed, you should use applyResourceMapping mode to build the new apk, otherwise, it may be crash at some times") ); } } /*else if (config.mUseApplyResource) { int totalChangeSize = addedSet.size() + modifiedSet.size() + largeModifiedSet.size(); if (totalChangeSize == 1 && resourceArscChanged) { Logger.e("Warning: we are using applyResourceMapping mode to build the new apk, but there is only resources.arsc changed, you should ensure there is actually resource changed!"); } }*/ } //add delete set deletedSet.addAll(getDeletedResource(config.mTempUnzipOldDir, config.mTempUnzipNewDir)); //we can't modify AndroidManifest file addedSet.remove(TypedValue.RES_MANIFEST); deletedSet.remove(TypedValue.RES_MANIFEST); modifiedSet.remove(TypedValue.RES_MANIFEST); largeModifiedSet.remove(TypedValue.RES_MANIFEST); //remove add, delete or modified if they are in ignore change pattern also removeIgnoreChangeFile(modifiedSet); removeIgnoreChangeFile(deletedSet); removeIgnoreChangeFile(addedSet); removeIgnoreChangeFile(largeModifiedSet); // last add test res in assets for user cannot ignore it; addAssetsFileForTestResource(); File tempResZip = new File(config.mOutFolder + File.separator + TEMP_RES_ZIP); final File tempResFiles = config.mTempResultDir; //gen zip resources_out.zip FileOperation.zipInputDir(tempResFiles, tempResZip, null); File extractToZip = new File(config.mOutFolder + File.separator + TypedValue.RES_OUT); String resZipMd5 = Utils.genResOutputFile(extractToZip, tempResZip, config, addedSet, modifiedSet, deletedSet, largeModifiedSet, largeModifiedMap); Logger.e("Final normal zip resource: %s, size=%d, md5=%s", extractToZip.getName(), extractToZip.length(), resZipMd5); logWriter.writeLineToInfoFile( String.format("Final normal zip resource: %s, size=%d, md5=%s", extractToZip.getName(), extractToZip.length(), resZipMd5) ); //delete temp file FileOperation.deleteFile(tempResZip); //gen zip resources_out_7z.zip File extractTo7Zip = new File(config.mOutFolder + File.separator + TypedValue.RES_OUT_7ZIP); File tempRes7Zip = new File(config.mOutFolder + File.separator + TEMP_RES_7ZIP); //ensure 7zip is enable if (FileOperation.sevenZipInputDir(tempResFiles, tempRes7Zip, config)) { //7zip whether actual exist if (tempRes7Zip.exists()) { String res7zipMd5 = Utils.genResOutputFile(extractTo7Zip, tempRes7Zip, config, addedSet, modifiedSet, deletedSet, largeModifiedSet, largeModifiedMap); //delete temp file FileOperation.deleteFile(tempRes7Zip); Logger.e("Final 7zip resource: %s, size=%d, md5=%s", extractTo7Zip.getName(), extractTo7Zip.length(), res7zipMd5); logWriter.writeLineToInfoFile( String.format("Final 7zip resource: %s, size=%d, md5=%s", extractTo7Zip.getName(), extractTo7Zip.length(), res7zipMd5) ); } } //first, write resource meta first //use resources.arsc's base crc to identify base.apk String arscBaseCrc = FileOperation.getZipEntryCrc(config.mOldApkFile, TypedValue.RES_ARSC); String arscMd5 = FileOperation.getZipEntryMd5(extractToZip, TypedValue.RES_ARSC); if (arscBaseCrc == null || arscMd5 == null) { throw new TinkerPatchException("can't find resources.arsc's base crc or md5"); } String resourceMeta = Utils.getResourceMeta(arscBaseCrc, arscMd5); writeMetaFile(resourceMeta); //pattern String patternMeta = TypedValue.PATTERN_TITLE; HashSet<String> patterns = new HashSet<>(config.mResRawPattern); //we will process them separate patterns.remove(TypedValue.RES_MANIFEST); writeMetaFile(patternMeta + patterns.size()); //write pattern for (String item : patterns) { writeMetaFile(item); } //write meta file, write large modify first writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD); writeMetaFile(modifiedSet, TypedValue.MOD); writeMetaFile(addedSet, TypedValue.ADD); writeMetaFile(deletedSet, TypedValue.DEL); } private void removeIgnoreChangeFile(ArrayList<String> array) { ArrayList<String> removeList = new ArrayList<>(); for (String name : array) { if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) { Logger.e("ignore change resource file: " + name); removeList.add(name); } } array.removeAll(removeList); } private void writeMetaFile(String line) { metaWriter.writeLineToInfoFile(line); } private void writeMetaFile(ArrayList<String> set, int mode) { if (!set.isEmpty()) { String title = ""; switch (mode) { case TypedValue.ADD: title = TypedValue.ADD_TITLE + set.size(); break; case TypedValue.MOD: title = TypedValue.MOD_TITLE + set.size(); break; case TypedValue.LARGE_MOD: title = TypedValue.LARGE_MOD_TITLE + set.size(); break; case TypedValue.DEL: title = TypedValue.DEL_TITLE + set.size(); break; } metaWriter.writeLineToInfoFile(title); for (String name : set) { String line = name; if (mode == TypedValue.LARGE_MOD) { LargeModeInfo info = largeModifiedMap.get(name); line = name + "," + info.md5 + "," + info.crc; } metaWriter.writeLineToInfoFile(line); } } } public ArrayList<String> getDeletedResource(File oldApkDir, File newApkDir) throws IOException { //get deleted resource DeletedResVisitor deletedResVisitor = new DeletedResVisitor(config, newApkDir.toPath(), oldApkDir.toPath()); Files.walkFileTree(oldApkDir.toPath(), deletedResVisitor); return deletedResVisitor.deletedFiles; } public class LargeModeInfo { public File path = null; public long crc; public String md5 = null; } class DeletedResVisitor extends SimpleFileVisitor<Path> { Configuration config; Path newApkPath; Path oldApkPath; ArrayList<String> deletedFiles; DeletedResVisitor(Configuration config, Path newPath, Path oldPath) { this.config = config; this.newApkPath = newPath; this.oldApkPath = oldPath; this.deletedFiles = new ArrayList<>(); } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Path relativePath = oldApkPath.relativize(file); Path newPath = newApkPath.resolve(relativePath); String patternKey = relativePath.toString().replace("\\", "/"); if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) { //not contain in new path, is deleted if (!newPath.toFile().exists()) { deletedFiles.add(patternKey); writeResLog(newPath.toFile(), file.toFile(), TypedValue.DEL); } return FileVisitResult.CONTINUE; } return FileVisitResult.CONTINUE; } } }