Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save panpf/4f04be745a921ff91b0ad8b95297f53d to your computer and use it in GitHub Desktop.
Save panpf/4f04be745a921ff91b0ad8b95297f53d to your computer and use it in GitHub Desktop.
一种通过ZIP备注实现APK多渠道的方法
import java.io.*;
import java.util.LinkedList;
import java.util.List;
public class BatchConfiguration {
public int version = -1;
public List<MCPComment> commentList;
private BatchConfiguration() {
}
public void addComment(MCPComment comment) {
if (comment.name == null || "".equals(comment.name.trim())) {
throw new IllegalArgumentException("comment name is null");
}
if (comment.attrList == null) {
throw new IllegalArgumentException("comment " + comment.name + " attrList is null");
}
if (commentList == null) {
commentList = new LinkedList<>();
}
commentList.add(comment);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(version);
if (commentList != null && commentList.size() > 0) {
for (MCPComment comment : commentList) {
builder.append("\n");
builder.append("\n");
builder.append(comment.name);
if (comment.attrList != null && comment.attrList.size() > 0) {
for (MCPComment.Attr attr : comment.attrList) {
builder.append("\n");
builder.append(attr.key).append("=").append(attr.value);
}
}
}
}
return builder.toString();
}
public static BatchConfiguration readFromFile(File file) throws Exception {
BufferedReader reader = new BufferedReader(new FileReader(file));
int lineNumber = 0;
int commentLineNumber = 0;
BatchConfiguration batchConfiguration = new BatchConfiguration();
MCPComment currentComment = null;
try {
while (true) {
String lineContent = reader.readLine();
if (lineContent == null) {
break;
}
lineContent = lineContent.trim();
lineNumber++;
if (lineNumber == 1) {
// 第一行是配置文件版本号
try {
batchConfiguration.version = Integer.valueOf(lineContent);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Configuration file is only a pure digital version number: " + lineContent + ". line: " + lineNumber);
}
} else if ("".equals(lineContent)) {
// 空行是Comment分割线,要把上一个Comment加入列表
if (currentComment != null) {
if (currentComment.name == null || "".equals(currentComment.name.trim())) {
throw new IllegalArgumentException("Comment name is empty: " + currentComment.toString());
}
if (currentComment.attrList == null) {
throw new IllegalArgumentException("Comment no attribute: " + currentComment.toString());
}
batchConfiguration.addComment(currentComment);
currentComment = null;
}
} else {
// 到这里就是Comment的内容了,要创建备注对象并初始化备注行号
if (currentComment == null) {
currentComment = new MCPComment();
commentLineNumber = 0;
}
// 备注行号加1,备注的第一行是备注名称、备注版本以及密码
commentLineNumber++;
if (commentLineNumber == 1) {
String[] elements = lineContent.split(" ");
if (elements.length >= 3) {
currentComment.name = elements[0];
String commentVersion = elements[1];
try {
currentComment.version = Integer.valueOf(commentVersion);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Comment version only digital: " + lineContent + ". line: " + lineNumber);
}
currentComment.password = elements[2];
} else {
throw new IllegalArgumentException("Comment name error format: " + lineContent + ". line: " + lineNumber);
}
} else {
int index = lineContent.indexOf('=');
if(index == -1){
throw new IllegalArgumentException("Comments attribute can only be keys to format: " + lineContent + ". line: " + lineNumber);
}
String attrKey = lineContent.substring(0, index).trim();
if ("".equals(attrKey)) {
throw new IllegalArgumentException("Comments Attribute key cannot be empty: " + lineContent + ". line: " + lineNumber);
}
String attrValue = lineContent.substring(index + 1).trim();
if ("".equals(attrValue)) {
throw new IllegalArgumentException("Comments Attribute value cannot be empty: " + lineContent + ". line: " + lineNumber);
}
currentComment.addAttr(new MCPComment.Attr(attrKey, attrValue));
}
}
}
if (currentComment != null) {
batchConfiguration.addComment(currentComment);
}
return batchConfiguration;
} finally {
close(reader);
}
}
private static void close(Closeable closeable) {
if (closeable == null) {
return;
}
if (closeable instanceof OutputStream) {
try {
((OutputStream) closeable).flush();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
import org.json.JSONException;
import java.io.File;
public class BatchPackager {
public static void main(String[] args) {
String configFilePath = args != null && args.length >= 1 ? args[0] : null;
if (configFilePath == null) {
System.out.println("请输入配置文件路径");
return;
}
File configFile = new File(configFilePath);
if(!configFile.exists()){
System.out.println("找不到配置文件:" + configFile.getPath());
return;
}
String apkFilePath = args.length >= 2 ? args[1] : null;
if (apkFilePath == null) {
System.out.println("请输入原始APK文件路径");
return;
}
File apkFile = new File(apkFilePath);
if(!apkFile.exists()){
System.out.println("找不到APK文件:" + apkFile.getPath());
return;
}
try {
build(apkFile, configFile);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void build(File originApkFile, File configurationFile) throws Exception {
long startTime = System.currentTimeMillis();
BatchConfiguration batchConfiguration = BatchConfiguration.readFromFile(configurationFile);
if (batchConfiguration.commentList == null || batchConfiguration.commentList.size() == 0) {
throw new IllegalArgumentException("Not found comment info in configuration file: " + configurationFile.getPath());
}
String apkFileName = originApkFile.getName();
int dotIndex = apkFileName.lastIndexOf('.');
String prefix = apkFileName.substring(0, dotIndex);
String suffix = apkFileName.substring(dotIndex);
File outDir = new File(originApkFile.getParent());
for (MCPComment comment : batchConfiguration.commentList) {
File newApkFile = new File(outDir, prefix + "-" + comment.name + suffix);
if (newApkFile.exists()) {
if (!newApkFile.delete()) {
throw new IllegalStateException("Unable to delete file: " + newApkFile.getPath());
}
}
MCPTool.nioTransferCopy(originApkFile, newApkFile);
String commentString = comment.getCommentString();
MCPTool.write(newApkFile, commentString, comment.password);
System.out.println(newApkFile.getPath() + ": " + commentString);
}
long useTime = System.currentTimeMillis() - startTime;
System.out.println();
System.out.println("Batch package finished, " + batchConfiguration.commentList.size() + " packages total time: " + (useTime / 1000) + " secs");
}
public static void read(File dir, String password) {
File[] childFiles = dir.listFiles();
if (childFiles == null || childFiles.length == 0) {
return;
}
for (File childFile : childFiles) {
String commentString = MCPTool.readContent(childFile, password);
if (commentString != null) {
MCPComment comment;
try {
comment = MCPComment.parse(commentString);
} catch (JSONException e) {
e.printStackTrace();
continue;
}
if (comment != null) {
System.out.println(childFile.getName() + "\n" + comment.getShowString());
} else {
System.out.println("parse comment info failed from " + childFile.getName() + " (" + commentString + ")");
}
} else {
System.out.println("not found comment info from " + childFile.getName());
}
System.out.print("\n");
}
}
}
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class MCPComment {
public String name;
public int version;
public String password;
public List<Attr> attrList;
public void addAttr(Attr attr) {
if (attr.key == null || "".equals(attr.key)) {
throw new IllegalArgumentException("attr key is null or empty");
}
if (attr.value == null || "".equals(attr.value)) {
throw new IllegalArgumentException("attr value is null or empty");
}
if (attrList == null) {
attrList = new LinkedList<>();
}
attrList.add(attr);
}
public String getCommentString() {
if (attrList != null && attrList.size() > 0) {
StringBuilder builder = new StringBuilder();
builder.append("{");
// 版本
builder.append("\"").append("version").append("\"");
builder.append(":").append(version);
// 属性
for (Attr attr : attrList) {
if (builder.length() > 1) {
builder.append(",");
}
builder.append("\"").append(attr.key).append("\"");
builder.append(":");
builder.append("\"").append(attr.value).append("\"");
}
builder.append("}");
return builder.toString();
} else {
return null;
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(name).append(" ").append(version).append(" ").append(password);
if (attrList != null && attrList.size() > 0) {
for (Attr attr : attrList) {
if (builder.length() > 0) {
builder.append("\n");
}
builder.append(attr.key).append("=").append(attr.value);
}
}
return builder.toString();
}
public String getShowString() {
StringBuilder builder = new StringBuilder();
builder.append("version").append("=").append(version);
if (attrList != null && attrList.size() > 0) {
for (Attr attr : attrList) {
if (builder.length() > 0) {
builder.append("\n");
}
builder.append(attr.key).append("=").append(attr.value);
}
}
return builder.toString();
}
public static class Attr {
public String key;
public String value;
public Attr(String key, String value) {
this.key = key;
this.value = value;
}
}
public static MCPComment parse(String commentJson) throws JSONException {
JSONObject jsonObject = new JSONObject(commentJson);
if (jsonObject.length() <= 0) {
return null;
}
MCPComment comment = new MCPComment();
Iterator<String> keyIterator = jsonObject.keys();
while (keyIterator.hasNext()) {
String key = keyIterator.next();
String value = jsonObject.getString(key);
if ("version".equals(key)) {
comment.version = Integer.valueOf(value);
} else {
if (comment.attrList == null) {
comment.attrList = new LinkedList<>();
}
comment.attrList.add(new Attr(key, jsonObject.getString(key)));
}
}
return comment;
}
}
/*
* Copyright (C) 2014 seven456@gmail.com
*
* 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.
*/
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.security.Key;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipFile;
/**
* 多渠道打包工具;<br/>
* 利用的是Zip文件“可以添加comment(注释)”的数据结构特点,在文件的末尾写入任意数据,而不用重新解压zip文件(apk文件就是zip文件格式);<br/>
* 创建时间: 2014-12-16 18:56:29
* @author zhangguojun
* @version 1.1
* @since JDK1.7 Android2.2
*/
public class MCPTool {
/**
* 数据结构体的签名标记
*/
private static final String SIG = "MCPT";
/**
* 数据结构的版本号
*/
private static final String VERSION_1_1 = "1.1";
/**
* 数据编码格式
*/
private static final String CHARSET_NAME = "UTF-8";
/**
* 加密用的IvParameterSpec参数
*/
private static final byte[] IV = new byte[] { 1, 3, 1, 4, 5, 2, 0, 1 };
/**
* 写入数据
* @param path 文件路径
* @param content 写入的内容
* @param password 加密密钥
* @throws Exception
*/
public static void write(File path, String content, String password) throws Exception {
write(path, content.getBytes(CHARSET_NAME), password);
}
/**
* 写入数据(如:渠道号)
* @param path 文件路径
* @param content 写入的内容
* @param password 加密密钥
* @throws Exception
*/
private static void write(File path, byte[] content, String password) throws Exception {
ZipFile zipFile = new ZipFile(path);
boolean isIncludeComment = zipFile.getComment() != null;
zipFile.close();
if (isIncludeComment) {
throw new IllegalStateException("Zip comment is exists, Repeated write is not recommended.");
}
boolean isEncrypt = password != null && password.length() > 0;
byte[] bytesContent = isEncrypt ? encrypt(password, content) : content;
byte[] bytesVersion = VERSION_1_1.getBytes(CHARSET_NAME);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(bytesContent); // 写入内容;
baos.write(short2Stream((short) bytesContent.length)); // 写入内容长度;
baos.write(isEncrypt ? 1 : 0); // 写入是否加密标示;
baos.write(bytesVersion); // 写入版本号;
baos.write(short2Stream((short) bytesVersion.length)); // 写入版本号长度;
baos.write(SIG.getBytes(CHARSET_NAME)); // 写入SIG标记;
byte[] data = baos.toByteArray();
baos.close();
if (data.length > Short.MAX_VALUE) {
throw new IllegalStateException("Zip comment length > 32767.");
}
// Zip文件末尾数据结构:{@see java.util.zip.ZipOutputStream.writeEND}
RandomAccessFile raf = new RandomAccessFile(path, "rw");
raf.seek(path.length() - 2); // comment长度是short类型
raf.write(short2Stream((short) data.length)); // 重新写入comment长度,注意Android apk文件使用的是ByteOrder.LITTLE_ENDIAN(小端序);
raf.write(data);
raf.close();
}
/**
* 读取数据
* @param path 文件路径
* @param password 解密密钥
* @return 被该工具写入的数据(如:渠道号)
* @throws Exception
*/
private static byte[] read(File path, String password) throws Exception {
byte[] bytesContent = null;
byte[] bytes;
RandomAccessFile raf = new RandomAccessFile(path, "r");
Object[] versions = getVersion(raf);
long index = (long) versions[0];
String version = (String) versions[1];
if (VERSION_1_1.equals(version)) {
bytes = new byte[1];
index -= bytes.length;
readFully(raf, index, bytes); // 读取内容长度;
boolean isEncrypt = bytes[0] == 1;
bytes = new byte[2];
index -= bytes.length;
readFully(raf, index, bytes); // 读取内容长度;
int lengthContent = stream2Short(bytes, 0);
bytesContent = new byte[lengthContent];
index -= lengthContent;
readFully(raf, index, bytesContent); // 读取内容;
if (isEncrypt && password != null && password.length() > 0) {
bytesContent = decrypt(password, bytesContent);
}
}
raf.close();
return bytesContent;
}
/**
* 读取数据结构的版本号
* @param raf RandomAccessFile
* @return 数组对象,[0] randomAccessFile.seek的index,[1] 数据结构的版本号
* @throws IOException
*/
private static Object[] getVersion(RandomAccessFile raf) throws IOException {
String version = null;
byte[] bytesMagic = SIG.getBytes(CHARSET_NAME);
byte[] bytes = new byte[bytesMagic.length];
long index = raf.length();
index -= bytesMagic.length;
readFully(raf, index, bytes); // 读取SIG标记;
if (Arrays.equals(bytes, bytesMagic)) {
bytes = new byte[2];
index -= bytes.length;
readFully(raf, index, bytes); // 读取版本号长度;
int lengthVersion = stream2Short(bytes, 0);
index -= lengthVersion;
byte[] bytesVersion = new byte[lengthVersion];
readFully(raf, index, bytesVersion); // 读取内容;
version = new String(bytesVersion, CHARSET_NAME);
}
return new Object[] { index, version };
}
/**
* RandomAccessFile seek and readFully
* @param raf
* @param index
* @param buffer
* @throws IOException
*/
private static void readFully(RandomAccessFile raf, long index, byte[] buffer) throws IOException {
raf.seek(index);
raf.readFully(buffer);
}
/**
* 读取数据(如:渠道号)
* @param path 文件路径
* @param password 解密密钥
* @return 被该工具写入的数据(如:渠道号)
*/
public static String readContent(File path, String password) {
try {
return new String(read(path, password), CHARSET_NAME);
} catch (Exception ignore) {
}
return null;
}
/**
* Android平台读取渠道号
* @param context Android中的android.content.Context对象
* @param mcptoolPassword mcptool解密密钥
* @param defValue 读取不到时用该值作为默认值
* @return
*/
public static String getChannelId(Object context, String mcptoolPassword, String defValue) {
String content = MCPTool.readContent(new File(getPackageCodePath(context)), mcptoolPassword);
return content == null || content.length() == 0 ? defValue : content;
}
/**
* 获取已安装apk文件的存储路径(这里使用反射,因为MCPTool项目本身不需要导入Android的运行库)
* @param context Android中的Context对象
* @return
*/
private static String getPackageCodePath(Object context) {
try {
return (String) context.getClass().getMethod("getPackageCodePath").invoke(context);
} catch (Exception ignore) {
}
return null;
}
/**
* 加密
* @param password
* @param content
* @return
* @throws Exception
*/
private static byte[] encrypt(String password, byte[] content) throws Exception {
return cipher(Cipher.ENCRYPT_MODE, password, content);
}
/**
* 解密
* @param password
* @param content
* @return
* @throws Exception
*/
private static byte[] decrypt(String password, byte[] content) throws Exception {
return cipher(Cipher.DECRYPT_MODE, password, content);
}
/**
* 加解密
* @param cipherMode
* @param password
* @param content
* @return
* @throws Exception
*/
private static byte[] cipher(int cipherMode, String password, byte[] content) throws Exception {
DESKeySpec dks = new DESKeySpec(password.getBytes(CHARSET_NAME));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
Key secretKey = keyFactory.generateSecret(dks);
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
IvParameterSpec spec = new IvParameterSpec(IV);
cipher.init(cipherMode, secretKey, spec);
return cipher.doFinal(content);
}
/**
* short转换成字节数组(小端序)
* @param data
* @return
*/
private static short stream2Short(byte[] stream, int offset) {
ByteBuffer buffer = ByteBuffer.allocate(2);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(stream[offset]);
buffer.put(stream[offset + 1]);
return buffer.getShort(0);
}
/**
* 字节数组转换成short(小端序)
* @param stream
* @param offset
* @return
*/
private static byte[] short2Stream(short data) {
ByteBuffer buffer = ByteBuffer.allocate(2);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort(data);
buffer.flip();
return buffer.array();
}
/**
* nio高速拷贝文件
* @param source
* @param target
* @return
* @throws IOException
*/
public static boolean nioTransferCopy(File source, File target) throws IOException {
FileChannel in = null;
FileChannel out = null;
FileInputStream inStream = null;
FileOutputStream outStream = null;
try {
File parent = target.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
inStream = new FileInputStream(source);
outStream = new FileOutputStream(target);
in = inStream.getChannel();
out = outStream.getChannel();
return in.transferTo(0, in.size(), out) == in.size();
} finally {
close(inStream);
close(in);
close(outStream);
close(out);
}
}
/**
* 关闭数据流
* @param closeable
*/
private static void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException ignore) {
}
}
}
// /**
// * 简单测试代码段
// * @param args
// * @throws Exception
// */
// public static void test() throws Exception {
// String content = "abc";
// String password = "123456789";
// System.out.println("content = " + content);
// String contentE = new String(encrypt(password, content.getBytes(CHARSET_NAME)), CHARSET_NAME);
// System.out.println("contentE = " + contentE);
// String contentD = new String(decrypt(password, contentE.getBytes(CHARSET_NAME)), CHARSET_NAME);
// System.out.println("contentD = " + contentD);
//
// }
/**
* jar命令行的入口方法
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// 写入渠道号
// args = "-path D:/111.apk -outdir D:/111/ -contents googleplay;m360; -password 12345678".split(" ");
// 查看工具程序版本号
// args = "-version".split(" ");
// 读取渠道号
// args = "-path D:/111_m360.apk -password 12345678".split(" ");
long time = System.currentTimeMillis();
String cmdPath = "-path";
String cmdOutdir = "-outdir";
String cmdContents = "-contents";
String cmdPassword = "-password";
String cmdVersion = "-version";
String help = "用法:java -jar MCPTool.jar [" + cmdPath + "] [arg0] [" + cmdOutdir + "] [arg1] [" + cmdContents + "] [arg2] [" + cmdPassword + "] [arg3]"
+ "\n" + cmdPath + " APK文件路径"
+ "\n" + cmdOutdir + " 输出路径(可选),默认输出到APK文件同一级目录"
+ "\n" + cmdContents + " 写入内容集合,多个内容之间用“;”分割(linux平台请在“;”前加“\\”转义符),如:googleplay;m360; 当没有" + cmdContents + "”参数时输出已有文件中的contents"
+ "\n" + cmdPassword + " 加密密钥(可选),长度8位以上,如果没有该参数,不加密"
+ "\n" + cmdVersion + " 显示MCPTool版本号"
+ "\n例如:"
+ "\n写入:java -jar MCPTool.jar -path D:/test.apk -outdir ./ -contents googleplay;m360; -password 12345678"
+ "\n读取:java -jar MCPTool.jar -path D:/test.apk -password 12345678";
if (args.length == 0 || args[0] == null || args[0].trim().length() == 0) {
System.out.println(help);
} else {
if (args.length > 0) {
if (args.length == 1 && cmdVersion.equals(args[0])) {
System.out.println("version: " + VERSION_1_1);
} else {
Map<String, String> argsMap = new LinkedHashMap<String, String>();
for (int i = 0; i < args.length; i += 2) {
if (i + 1 < args.length) {
if (args[i + 1].startsWith("-")) {
throw new IllegalStateException("args is error, help: \n" + help);
} else {
argsMap.put(args[i], args[i + 1]);
}
}
}
System.out.println("argsMap = " + argsMap);
File path = argsMap.containsKey(cmdPath) ? new File(argsMap.get(cmdPath)) : null;
String parent = path == null? null : (path.getParent() == null ? "./" : path.getParent());
File outdir = parent == null ? null : new File(argsMap.containsKey(cmdOutdir) ? argsMap.get(cmdOutdir) : parent);
String[] contents = argsMap.containsKey(cmdContents) ? argsMap.get(cmdContents).split(";") : null;
String password = argsMap.get(cmdPassword);
if (path != null) {
System.out.println("path: " + path);
System.out.println("outdir: " + outdir);
if (contents != null && contents.length > 0) {
System.out.println("contents: " + Arrays.toString(contents));
}
System.out.println("password: " + password);
if (contents == null || contents.length == 0) { // 读取数据;
System.out.println("content: " + readContent(path, password));
} else { // 写入数据;
String fileName = path.getName();
int dot = fileName.lastIndexOf(".");
String prefix = fileName.substring(0, dot);
String suffix = fileName.substring(dot);
for (String content : contents) {
File target = new File(outdir, prefix + "_" + content + suffix);
if (nioTransferCopy(path, target)) {
write(target, content, password);
}
}
}
}
}
}
}
System.out.println("time:" + (System.currentTimeMillis() - time));
}
}
1
Test1 1 meispassword
UmengChannel=google_Play
custom1=1
Test2 1 meispassword
UmengChannel=yingyongbao
custom1=1
Test3 1 meispassword
UmengChannel=yingyongbao
custom1=1
Test4 1 meispassword
UmengChannel=yingyongbao
custom1=1
Test5 1 meispassword
UmengChannel=yingyongbao
custom1=1
Test6 1 meispassword
UmengChannel=yingyongbao
custom1=1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment