package com.taobao.atlas.update.util;
import android.os.Build;
import android.taobao.atlas.bundleInfo.AtlasBundleInfoManager;
import android.taobao.atlas.framework.Atlas;
import android.taobao.atlas.framework.BundleImpl;
import android.taobao.atlas.framework.Framework;
import android.taobao.atlas.runtime.RuntimeVariables;
import android.taobao.atlas.util.ApkUtils;
import android.taobao.atlas.versionInfo.BaselineInfoManager;
import android.text.TextUtils;
import android.util.Pair;
import com.taobao.atlas.dexmerge.DexMergeClient;
import com.taobao.atlas.dexmerge.MergeCallback;
import com.taobao.atlas.dexmerge.MergeObject;
import com.taobao.atlas.update.exception.MergeException;
import com.taobao.atlas.update.model.UpdateInfo;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Created by guanjie on 15/10/8.
*/
public class PatchMerger {
private UpdateInfo updateInfo;
private File patchFile;
private MergeCallback mergeCallback;
private ZipFile apkZip;
private static int BUFFEREDSIZE = 1024;
public Map<String, Pair> mergeOutputs = new HashMap<String, Pair>();
private static boolean supportMerge;
private static String MAIN_DEX = "com.taobao.maindex";
static {
supportMerge = Build.VERSION.SDK_INT < 21;
}
public PatchMerger(UpdateInfo updateInfo, File patchFile, MergeCallback mergeCallback) throws MergeException {
this.updateInfo = updateInfo;
this.patchFile = patchFile;
this.mergeCallback = mergeCallback;
try {
this.apkZip = new ZipFile(RuntimeVariables.androidApplication.getApplicationInfo().sourceDir);
} catch (IOException e) {
throw new MergeException(e);
}
}
public void merge() throws MergeException, IOException {
try {
File outputDirectory = updateInfo.workDir;
File oringnalDir = new File(outputDirectory.getParentFile(), "orignal_" + updateInfo.baseVersion);
if (!outputDirectory.exists()){
outputDirectory.mkdirs();
}
if (!oringnalDir.exists()){
oringnalDir.mkdirs();
}
ZipFile patchZip = new ZipFile(patchFile);
Pair<File, String>[] updateBundles = new Pair[updateInfo.updateBundles.size()];
List<MergeObject> toMergeList = new ArrayList<MergeObject>();
//find original bundle and store in pair struct
for (int x = 0; x < updateInfo.updateBundles.size(); x++) {
UpdateInfo.Item item = updateInfo.updateBundles.get(x);
String bundleName = item.name;
String entryNamePrefix = String.format("%s%s", "lib", bundleName.replace(".", "_"));
String entryName = entryNamePrefix + ".so";
ZipEntry entry = patchZip.getEntry(entryName);
if (patchZip.getEntry(entryName) != null && !supportMerge(bundleName)) {
//全量部署
File targetBundle = new File(outputDirectory, entryName);
OutputStream outputStream = new FileOutputStream(targetBundle);
InputStream inputStream = patchZip.getInputStream(entry);
copyStream(inputStream, outputStream);
mergeOutputs.put(bundleName, new Pair<>(targetBundle.getAbsolutePath(), item.version));
} else {
//差量部署
File originalBundle = findOriginalBundleFile(bundleName, oringnalDir.getAbsolutePath(), item);
if (originalBundle != null && originalBundle.exists()) {
updateBundles[x] = new Pair<File, String>(originalBundle, bundleName);
File targetBundle = new File(outputDirectory, "lib" + bundleName.replace(".", "_") + ".so");
if (targetBundle.exists()) {
targetBundle.delete();
}
toMergeList.add(new MergeObject(updateBundles[x].first.getAbsolutePath(), updateBundles[x].second, targetBundle.getAbsolutePath()));
mergeOutputs.put(bundleName, new Pair<>(targetBundle.getAbsolutePath(), item.version));
}
}
}
if (toMergeList.size() > 0) {
DexMergeClient dexMergeClient = getMergeClient();
boolean mergeFinish = dexMergeClient.dexMerge(patchFile.getAbsolutePath(),toMergeList, true);
dexMergeClient.unPrepare();
if (!mergeFinish) {
throw new MergeException("merge failed!");
}
}
} catch (Exception e){
e.printStackTrace();
throw new MergeException("merge failed!");
} finally{
if (apkZip != null) {
try {
apkZip.close();
} catch (Throwable e) {
}
}
}
}
/**
* be not call in main thread
*/
/**
* get original bundle
*
* @param bundleName
* @return
*/
public File findOriginalBundleFile(String bundleName, String bundleDirIfNeedCreate, UpdateInfo.Item item) throws IOException {
if (bundleName.equals(MAIN_DEX)){
return new File(RuntimeVariables.androidApplication.getApplicationInfo().sourceDir);
}
if (!TextUtils.isEmpty(bundleName)) {
File oldBundle = null;
if (TextUtils.isEmpty(item.srcVersion)) {
oldBundle = Atlas.getInstance().getBundleFile(bundleName) != null ? Atlas.getInstance().getBundleFile(bundleName) : Framework.getInstalledBundle(bundleName, "");
} else {
BundleImpl impl = (BundleImpl) Atlas.getInstance().getBundle(bundleName);
if (impl != null && !BaselineInfoManager.instance().isDexPatched(bundleName)) {
String version = impl.getArchive().getCurrentRevision().getVersion();
if (version != null && version.equals(item.srcVersion)) {
oldBundle = impl.getArchive().getArchiveFile();
} else if (version != null && !version.equals(item.srcVersion) && !AtlasBundleInfoManager.instance().isInternalBundle(bundleName)) {
// Atlas.getInstance().restoreBundle(new String[]{bundleName});
return null;
} else if (version != null && !version.equals(item.srcVersion)) {
throw new IOException("can not find valid src bundle of " + bundleName);
}
} else {
oldBundle = Framework.getInstalledBundle(bundleName, item.srcVersion);
if (oldBundle == null) {
throw new IOException("can not find valid src bundle of " + bundleName);
}
}
}
if (oldBundle == null) {
String oldBundleFileName = String.format("lib%s.so", bundleName.replace(".", "_"));
File libDir = new File(RuntimeVariables.androidApplication.getFilesDir().getParentFile(), "lib");
oldBundle = new File(libDir,
oldBundleFileName);
if (oldBundle.exists()) {
return oldBundle;
} else {
if (apkZip == null) {
apkZip = new ZipFile(RuntimeVariables.androidApplication.getApplicationInfo().sourceDir);
}
String entryName = String.format("lib/armeabi/%s", oldBundleFileName);
if (apkZip.getEntry(entryName) != null) {
InputStream inputStream = apkZip.getInputStream(apkZip.getEntry(entryName));
oldBundle = new File(bundleDirIfNeedCreate, oldBundleFileName);
// oldBundle.createNewFile();
ApkUtils.copyInputStreamToFile(inputStream, oldBundle);
return oldBundle;
}
}
}
return oldBundle;
}
return null;
}
private DexMergeClient getMergeClient() {
DexMergeClient mergeClient = new DexMergeClient(mergeCallback);
boolean result = mergeClient.prepare();
if (result) {
return mergeClient;
}
throw new RuntimeException("prepare client error");
}
private boolean supportMerge(String bundleName) {
return supportMerge&&bundleName.equals(MAIN_DEX);
}
private void copyStream(InputStream in, OutputStream out) throws IOException {
try {
int c;
byte[] by = new byte[BUFFEREDSIZE];
while ((c = in.read(by)) != -1) {
out.write(by, 0, c);
}
out.flush();
} catch (IOException e) {
throw e;
} finally {
closeQuitely(out);
closeQuitely(in);
}
}
private void closeQuitely(Closeable stream) {
try {
if (stream != null)
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}